[fix] Refuse an automatic session rename over a name a person typed [AGE-4287] - #6676
Conversation
An approval card parks the agent's rename_session call. The person renames the session by hand while it waits. The deferred call then runs with the name the agent chose before the rename, so the person's name is replaced and the agent reports the stale one. Nothing in the agent's own view separates a stale intent from a fresh one, so the row answers instead. A header edit now says who chose the name: author=user for a person typing one, author=auto for a program proposing one. A person's name is stamped on the row as a reserved ag. tag, and an auto edit that would replace it is refused with 409 carrying the current name, so the agent adopts it. author is a query parameter, never a body field. The rename_session catalog entry fixes author=auto inside its own path and the model fills only the body, so an agent cannot claim a person chose its name. The browser's auto-title sends author=auto too, so the agent can still name a session over it. override_user_name in the body is the way past the refusal, for the rename a person actually asked for. Fixes #6657
|
@coderabbitai review |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📘 Docs preview
This comment updates in place on every push. |
|
Warning Review limit reachedNext included review available in 14 minutes. View limit detailsLimit details: You’ve used all 8 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 0 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughSession names now record their source and revision. Automatic renames require matching replacement data and cannot replace manual names after stale or concurrent updates. The API, clients, persistence layer, catalog, documentation, and tests support this behavior. ChangesSession rename protection
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Severity of issue fixed: Medium Merge Risk: 🔵 Low · up to Session renames now preserve manually chosen names against stale automatic updates and return revision-aware replacement details. The remaining risk is limited to a web comment-format convention and does not affect runtime behavior. Sequence Diagram(s)sequenceDiagram
participant Agent
participant SessionStreamsRouter
participant SessionStreamsService
participant SessionStreamsDAO
Agent->>SessionStreamsRouter: rename_session with name_source=automatic
SessionStreamsRouter->>SessionStreamsService: set_header with replacement fields
SessionStreamsService->>SessionStreamsDAO: update_header
SessionStreamsDAO->>SessionStreamsDAO: lock row and evaluate name state
SessionStreamsRouter-->>Agent: HTTP 409 envelope or successful rename
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
Railway Preview Environment
Updated at 2026-09-08T21:53:53.249Z |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
web/packages/agenta-entities/src/session/api/api.ts (1)
838-839: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCondense both comments to one short line.
web/AGENTS.mdapplies to both cited files and sets this as a hard rule. Keep only the essentialauthorexplanation at each site.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: 8af2974b-4800-475a-a072-1776421ce6df
📒 Files selected for processing (16)
api/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/core/sessions/streams/dtos.pyapi/oss/src/core/sessions/streams/interfaces.pyapi/oss/src/core/sessions/streams/service.pyapi/oss/src/core/sessions/streams/types.pyapi/oss/src/dbs/postgres/sessions/streams/dao.pyapi/oss/src/dbs/postgres/sessions/streams/mappings.pyapi/oss/tests/pytest/unit/sessions/test_stream_fill_once.pyapi/oss/tests/pytest/unit/sessions/test_stream_header_merge.pyapi/oss/tests/pytest/unit/sessions/test_stream_rename_guard.pydocs/design/agent-workflows/documentation/tools.mdsdks/python/agenta/sdk/agents/platform/op_catalog.pysdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.pyservices/runner/tests/unit/tool-direct.test.tsweb/oss/src/components/AgentChatSlice/state/sessions.tsweb/packages/agenta-entities/src/session/api/api.ts
Included review availability: Your plan provides up to 8 included reviews per hour; 0 remain after this review.
|
…ind the override to a name Three changes from review. An automatic edit no longer moves the name-source stamp at all. It used to clear it on every edit that carried a name, so the agent repeating the name a person typed - which the rule allows, because it changes nothing - left the row unprotected and the next automatic rename replaced the person's name. The guard now runs inside the DAO's write transaction, against a row locked with FOR UPDATE. A check a layer up was a read-then-write: a person's rename could commit between the read and the write, and the automatic edit would replace a name that did not exist when it was checked. override_user_name is replaced by replacing_name, the exact name the edit replaces. A bare permission is timeless: an agent authorized to rename A can sit parked while the person renames to B, and approving it then overwrites B. A precondition cannot, because it stops matching. Also from review: author=user|auto is now name_source=manual|automatic, which names where a name came from rather than who called; the tag encoding moved out of the core DTOs into the postgres mapper and the rule into its own core module; the 409 carries the agent-actionable envelope from api/AGENTS.md instead of a bare string; and SessionStream no longer exposes the decoded source, since only the write path reads it. Adds a Postgres integration suite. One cell holds the row's lock and asserts the automatic write waits for it, which fails without FOR UPDATE.
|
@coderabbitai review |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
web/packages/agenta-entities/src/session/api/api.ts (1)
810-813: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShorten the four
nameSourcecomments to one short line each.web/AGENTS.mddefines this as an explicit web contract. ESLint and Prettier do not enforce comment length, so this is a maintainability cleanup rather than a CI failure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: ea4d5b80-06b5-4b83-8176-e6a5ffdeb0c0
📒 Files selected for processing (19)
api/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/core/sessions/streams/dtos.pyapi/oss/src/core/sessions/streams/interfaces.pyapi/oss/src/core/sessions/streams/naming.pyapi/oss/src/core/sessions/streams/service.pyapi/oss/src/core/sessions/streams/types.pyapi/oss/src/dbs/postgres/sessions/streams/dao.pyapi/oss/src/dbs/postgres/sessions/streams/mappings.pyapi/oss/tests/pytest/unit/sessions/test_stream_fill_once.pyapi/oss/tests/pytest/unit/sessions/test_stream_header_merge.pyapi/oss/tests/pytest/unit/sessions/test_stream_rename_guard.pyapi/oss/tests/pytest/unit/sessions/test_stream_rename_guard_integration.pydocs/design/agent-workflows/documentation/tools.mdsdks/python/agenta/sdk/agents/platform/op_catalog.pysdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.pyservices/runner/tests/unit/tool-direct.test.tsweb/oss/src/components/AgentChatSlice/state/sessions.tsweb/packages/agenta-entities/src/session/api/api.tsweb/packages/agenta-sessions-ui/src/useSessionActions.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- docs/design/agent-workflows/documentation/tools.md
- api/oss/src/dbs/postgres/sessions/streams/dao.py
- api/oss/src/core/sessions/streams/service.py
Included review availability: Your plan provides up to 8 included reviews per hour; 0 remain after this review.
|
…tomatic clear
Two more holes from the Codex re-pass, both of which let an automatic write
weaken the protection it had just been checked against.
A successful authorized rename could replay. The person names the session A,
asks the agent for B, gets B, then puts A back themselves. Replaying the same
call took B again, because the name matched a second time. The row now carries
a name revision that counts every name change, and the precondition names both
the name and the revision it was read at. A restored name brings back the
string, never the revision, so the authorization is spent.
Both halves are load-bearing. A revision counts from one and is guessable; a
name is not. A name repeats; a revision does not.
An automatic caller could clear the name. On a person-named session, an
authorized {name: ""} left the row with an empty name, and every later
automatic rename walked past the guard, because a row with no name has nothing
to protect. Only a person removes a name now.
Also: FOR NO KEY UPDATE rather than FOR UPDATE, so a rename does not contend
with the KEY SHARE lock a session_turns insert takes on this row; and the
refusal message bounds the name it repeats, so a long name cannot push the
machine-readable half of the envelope past the runner's 2000-character cut.
Web: the browser auto-title now drops its optimistic title when the server
refuses the write, instead of showing a name the server rejected until the next
list poll disagrees.
A warm agent session holds the tool descriptors it opened with, so a sandbox started under an earlier build of this change can still execute ?author=auto after the service has moved on. Reading it costs one branch and keeps that call on the guarded path; the alternative is a rename that silently claims a person made it. An explicit name_source always wins, so the alias cannot override a caller that speaks the current spelling, and an unrecognized value falls through to the default rather than failing the rename. The parameter is hidden from the schema and marked deprecated, and can be deleted once no session that old can be alive. This never shipped in a release: git log --all -S 'author=auto' returns only this PR's own commits.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: e6189080-8d59-4890-91e9-5a1f3ffc4a31
📒 Files selected for processing (10)
api/oss/src/core/sessions/streams/dtos.pyapi/oss/src/core/sessions/streams/naming.pyapi/oss/src/core/sessions/streams/types.pyapi/oss/src/dbs/postgres/sessions/streams/dao.pyapi/oss/src/dbs/postgres/sessions/streams/mappings.pyapi/oss/tests/pytest/unit/sessions/test_stream_rename_guard.pyapi/oss/tests/pytest/unit/sessions/test_stream_rename_guard_integration.pysdks/python/agenta/sdk/agents/platform/op_catalog.pysdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.pyweb/oss/src/components/AgentChatSlice/state/sessions.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- api/oss/src/core/sessions/streams/dtos.py
- web/oss/src/components/AgentChatSlice/state/sessions.ts
- sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py
Included review availability: Your plan provides up to 8 included reviews per hour; 0 remain after this review.
A rename can store a name longer than the prose bound, and replacing_name has to match the stored value exactly. A caller that copied a shortened quote out of the message was refused forever with no way to tell why, so a long person-chosen name became impossible to replace legitimately. The sentences no longer repeat a name they cannot repeat whole: past the bound they stop naming the session and point at details.current_name instead. Every next_step now names details.current_name and details.name_revision rather than the prose above it. The revision comes first in details, so the runner's 2000-character cut of the serialized detail reaches the small field before a long name can crowd it out. Two follow-ups from the Codex pass, both non-blocking there. replacing_revision now accepts 0. A title filled from a session's first message carries no revision, so a person who keeps that title owns a name nobody counted; rejecting 0 made that one session impossible to rename on request. The browser auto-title clears its optimistic title only when the server REFUSED the write, not when the write merely failed. A refusal means the server holds a name a person chose. A network failure means nobody knows yet, and blanking the row then would lose a title for no reason.
Context
Fixes #6657. With permissions set to Ask, the agent queues a
rename_sessioncall and the turn parks on an approval card. You rename the session by hand while the card waits. You approve. The deferred rename then runs with the arguments the agent chose before your rename, your name is gone, and the agent reports the name it wrote.The agent's own view cannot tell a stale intent from a fresh one. It renamed because it decided to, and by the time the call runs that decision is old. #6667 makes the agent see the session's current name on every new turn, which helps, but a parked approval continues the pending prompt and the queued call already holds its old arguments.
Changes
The row answers the question the agent cannot. A header edit now says where the name came from, and the row remembers whether a person controls it.
PUT/POST /api/sessions/streams/headertakes aname_sourcequery parameter:name_sourcemanual(default)automaticrename_session, and the browser's auto-titleThe row also counts its name changes. The stamp and the counter are reserved
ag.tags, so there is no migration and no client ever reads them. Every existing reserved tag is already stripped from client responses by the same mapper.The decision is made inside the write transaction, against a row locked with
FOR NO KEY UPDATE. A check a layer up would be a read-then-write: a person's rename can commit between the read and the write, and the automatic edit would then replace a name that did not exist when it was checked. Both header writers queue on the row instead, so the loser re-reads the winner's name and is refused on it. The weaker lock mode is deliberate: asession_turnsinsert takes a KEY SHARE lock on this row for its foreign key, and a rename has no business making a turn wait.An automatic edit never moves the stamp, and cannot clear the name. Only a manual edit does either. Both matter because the rule lets some automatic edits through on a protected row, and any of them leaving the row less protected than it found it would hand the next stale call an open door.
The refusal carries the current state, in the agent-actionable envelope
api/AGENTS.mdrequires:The runner passes an object
detailthrough to the model, so the agent adopts the name instead of retrying. The message bounds the name it repeats, so a long name cannot pushdetailspast the runner's 2000-character cut.Three things are deliberately not refused. An edit that sets no name only touches the description. An edit whose name already matches the stored one changes nothing, so a retry is safe. And
replacing_namewithreplacing_revision, naming the exact state being replaced, passes while both still match. That is how "rename this session to X" works after you named it yourself.That pair is a precondition, not a permission, and both halves are load-bearing. A bare "yes, overwrite" flag would be timeless: an agent legitimately authorized to replace
Acan sit parked while you rename toB, and approving it then overwritesB. A stale call carries that flag as truthfully as a fresh one. The name alone is not enough either: rename toB, restoreA, and the same call matches a second time and takesBback. The revision counts every name change and never returns, so an authorization is spent on one state. The name is the half that is not guessable, since revisions count from one.name_sourceis a query parameter, never a body field. Therename_sessioncatalog entry fixesname_source=automaticinside its own path and the model fills only the body, so an agent cannot claim a person chose its name. The browser's auto-title sendsautomatictoo, which is what keeps the agent able to name a session over the title derived from your first message.Why this shape
The brief asked for three options in order.
(a) Refuse an automatic rename over a person's name. Chosen, with (b) folded in. No marker existed, so this PR adds one. A turn-scoped fence was the alternative and it does not work: the reported replay happens on the turn after the approval, whose start is already later than the by-hand rename, so a fence comparing the rename against the turn's start lets it straight through.
(c) Clear a parked
rename_sessionapproval when the person renames. Rejected. It covers only the parked case, and the same stale call arrives without an approval whenever a model retries from its own transcript.Tests
apiunitapirename-guard integration, real Postgressdks/pythonagents unitservicesunitservices/runnertool-directunittype-check(oss + ee), mobiletypes:check@agenta/ossunit@agenta/entities,@agenta/sessions-uiunittest_stream_rename_guard.pyhas 46 unit tests over the rule, the stamp, the service and the route, then the sequences that break if any one transition is subtly wrong: repeating a person's name does not open the door, an asked-for rename does not open the door, an authorization cannot be spent twice, and an automatic clear cannot strip the protection.test_stream_rename_guard_integration.pydrives the real DAO against real Postgres.test_a_rename_that_commits_mid_flight_is_not_overwrittenholds the row's lock in one transaction, asserts the automatic write waits for it, commits a person's rename, and then asserts the write is refused on the new name. Removing the row lock makes it fail, which I checked.ruff formatandruff checkclean at the CI-pinned 0.15.12.prettierclean.Live evidence
A standalone EE dev stack built from this branch, harness Pi core, model
openai/gpt-4.1-minithrough an OpenRouter connection, sandbox local. The journey is the issue's: permissionsask, a gated file write parks the turn and defers the queued call, the person renames by hand while the card waits, the card is approved, then a turn asks the agent what the session is called. Before is the same stack with the catalog path reverted to the branch point, so the only variable is the fix.file-create-and-shell-command-testQA-6657 parked renamefile-create-and-shell-command-testQA-6657 parked renameThe runner's own log shows the call and the refusal in the after run:
Two more cells on the same stack, both green:
rename_sessionlands and replaces the title derived from the first message, so self-naming is untouched.replacing_nameandreplacing_revisioncopied from it, and the stored name becomes the asked-for one in the same turn.What to QA
Alt+R, then approve. The tab keeps your name.Not covered
name_source, is treated as a person. That is the recoverable direction, not a free win: during a rolling deploy a service on an older SDK sends noname_source, so an automatic name it writes is remembered as person-chosen, and the next agent rename of that session is refused until a person renames it. The other default loses a person's name instead.current_nameandname_revisionout of the refusal and invents the request gets through. Both values are handed to it precisely so a genuine request can succeed. The tool description states the one case it is for.SessionStreamEditcan still carrynameand a wholetagsdict, so a future caller of the flag-mirror edit could overwrite the stamp. No live caller sets either field. Narrowing that DTO to the lifecycle fields its callers actually own is worth doing and is not in this PR.Review rounds
Round 2,
ed8cb6186b. Two Major CodeRabbit threads and three Codex P1s. An automatic edit with the same name cleared the stamp, which was deterministic: person names itA, agent submitsA, protection disappears, a stale call submitsBand succeeds. The guard was not atomic with the write. And the override was timeless. Fixed by making an automatic edit never move the stamp, moving the guard into the write transaction, and replacingoverride_user_namewith a precondition. Codex also asked for the naming to say what it means (author=user|autobecamename_source=manual|automatic, since both requests run under the same user credential and only the name is governed), for the storage encoding to leave the core, for the structured error envelope, and for no new field on the public response. All four done.Round 3,
e02f8a8d29. A Codex re-pass found two more, both cases of an allowed automatic write weakening the protection it had just passed. A successful authorized rename could replay after the person restored the earlier name, since the name matched a second time; the revision closes that. And an automatic caller could clear the name, leaving the row with nothing to protect; only a person clears a name now. Also from that pass:FOR NO KEY UPDATEinstead ofFOR UPDATE, and a bounded name echo in the refusal.CodeRabbit also asked for a legacy alias mapping
author=autoonto the new parameter. Declined, with the evidence in the thread:git log --all -S 'author=auto'returns only this PR's own two commits, so there is no producer anywhere that sends it and the alias would be dead on arrival.