[docs] Document session rewind and branching design - #5917
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdded a design workspace for session rewind and branching. The documents cover frontend fork recovery, durable session lineage, effective transcript reads, attachment access, implementation sequencing, performance, migration, and open decisions. ChangesSession rewind branching
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (3)
docs/design/session-rewind-branching/data-model.md (1)
230-244: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDefine keys and indexes for
session_attachment_access.The table has no primary key, unique constraint, or lookup index. Retries can create duplicate access rows, and attachment authorization can scan the table for every reference. Add uniqueness for
(project_id, session_id, attachment_id)and indexes for source-owner and deletion checks.docs/design/session-rewind-branching/performance-and-migration.md (2)
71-76: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMake the single-query requirement explicit.
bounded iterative querycan mean one database round trip per ancestor. That contradicts the stated one lineage-chain query and the requirement that depth must not increase query count. Specify a recursive CTE or a single batched query. Add query-count tests for depths 0, 1, 5, and the configured maximum.
122-123: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winState the parent-prefix immutability contract.
The cache plan assumes that the parent prefix never changes. The documents also mention record lifecycle fields and ancestor soft deletion. If a retained record or attachment access can change, the effective transcript cache can serve stale or unauthorized content. Require stable inherited data while descendants exist, or add lineage-aware invalidation and versioning.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 6db91fa0-f4d4-436e-b96b-5f9223484abc
📒 Files selected for processing (8)
docs/design/session-rewind-branching/README.mddocs/design/session-rewind-branching/api-design.mddocs/design/session-rewind-branching/context.mddocs/design/session-rewind-branching/data-model.mddocs/design/session-rewind-branching/performance-and-migration.mddocs/design/session-rewind-branching/plan.mddocs/design/session-rewind-branching/research.mddocs/design/session-rewind-branching/status.md
| The retained messages remain in the existing local message store. The write rules are: | ||
|
|
||
| - Store edit or rerun mode when `rewindForkAtomFamily` creates the child. | ||
| - Store the restored user draft for edit mode. | ||
| - Restore the draft or automatic rerun behavior when the child mounts after reload. | ||
| - Read it when `buildAgentRequest` decides whether to disable last-message-only optimization. | ||
| - Mark it `in-flight` only when transport dispatch begins. | ||
| - Treat an `in-flight` bootstrap restored after page reload as `pending`, because the browser stream | ||
| that owned it no longer exists. | ||
| - Keep it through error, abort, and disconnect. | ||
| - Clear it only when the first request completes successfully. | ||
| - Delete it when the child session is permanently deleted. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## files matching api-design.md"
fd -a 'api-design\.md$' . | sed 's#^\./##'
echo "## relevant file excerpt"
if [ -f docs/design/session-rewind-branching/api-design.md ]; then
cat -n docs/design/session-rewind-branching/api-design.md | sed -n '1,120p'
fi
echo "## persist drainPersist references"
rg -n "drainPersist|persist|bootstrap|in-flight|pending|rewindForkAtomFamily|buildAgentRequest|onFinal|start|finish|onFinish|onError" services docs -S || true
echo "## package versions for ai"
for f in package.json apps services apps/* package-lock.json pnpm-lock.yaml yarn.lock; do
[ -f "$f" ] || continue
echo "--- $f"
rg -n '"ai"|ai@\|`@ai-sdk`|`@ai-sdk/`(.*)' "$f" | head -80 || true
doneRepository: Agenta-AI/agenta
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## candidate session persist source"
fd -a 'persist\.(ts|js)$' services package docs | sed 's#^\./##' || true
echo "## runner persist outline if available"
if [ -f services/runner/src/sessions/persist.ts ]; then
wc -l services/runner/src/sessions/persist.ts
ast-grep outline services/runner/src/sessions/persist.ts || true
cat -n services/runner/src/sessions/persist.ts | sed -n '1,260p'
fiRepository: Agenta-AI/agenta
Length of output: 14284
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## focused api-design excerpt"
sed -n '1,90p' docs/design/session-rewind-branching/api-design.md | cat -n
echo "## runner persist full remainder around buildPersistingEmitter"
sed -n '237,441p' services/runner/src/sessions/persist.ts | cat -n
echo "## ai SDK version pins"
rg -n -S '"ai"|"`@ai-sdk/`(.*)"|ai@|pnpm-lock.yaml|package-lock.json|yarn.lock' package.json package-lock.json pnpm-lock.yaml yarn.lock **/package.json 2>/dev/null | head -200 || true
echo "## references to bootstraps/session reload state"
rg -n -S "bootstrap|in-flight|pending|last-message|buildAgentRequest|rewindForkAtomFamily|drainPersist|takePersistFailures|recordsIncomplete" services/runner services api apps -S || trueRepository: Agenta-AI/agenta
Length of output: 50375
🌐 Web query:
Vercel AI SDK 3 onFinish onFinishStream completed finishReason error documentation callback fields
💡 Result:
In the Vercel AI SDK, onFinish (now primarily referred to as onEnd) is a lifecycle callback used to perform actions after a generation or stream completes [1][2][3]. Key details regarding this callback in the AI SDK include: Callback Name: While onFinish was commonly used in earlier versions, it has been deprecated in favor of onEnd [2][4][3]. Both serve the same purpose: triggering logic once the entire generation (including all steps) is complete [1][5]. Callback Fields: When the onEnd (or onFinish) callback is triggered, it receives an event object containing aggregated data about the completed generation [1][5]. Common fields include: - text: The final generated text [1][6]. - finishReason: The reason the generation stopped (e.g., 'stop', 'length', 'content-filter', 'tool-calls') [1][5][7]. - usage: Token usage statistics (LanguageModelUsage) [1][8]. - responseMessages: The messages generated during the call [1][6]. - steps: Details regarding intermediate steps [1][5]. - totalUsage: Total token usage across all steps [1][5]. Error Handling: - Callback Errors: If the onEnd/onFinish callback itself throws an error, it is caught internally by the AI SDK, and the operation continues [4]. - Streaming Errors: The onEnd callback is typically triggered only upon successful or complete stream termination [9]. For errors that occur during a stream, developers should use try/catch blocks for the stream operation or monitor stream parts (such as 'error' or 'tool-error' types) when iterating over the stream [9][10]. - Abort Handling: If a stream is aborted (e.g., by the user), onEnd is not called [9]. Instead, an onAbort callback is provided for cleanup operations [9][10]. Note: Ensure you are using a compatible version of the SDK, as onFinish/onEnd functionality evolved significantly in early 3.x releases [11]. Documentation recommends using onEnd and onStepEnd for new implementations [4][8].
Citations:
- 1: https://ai-sdk.dev/docs/ai-sdk-core/generating-text
- 2: https://ai-sdk.dev/docs/reference/ai-sdk-core/agent
- 3: https://github.com/vercel/ai/blob/a23b6767/packages/ai/src/generate-text/stream-text.ts
- 4: https://ai-play.vercel.app/docs/ai-sdk-core/lifecycle-callbacks
- 5: https://ai-play.vercel.app/docs/ai-sdk-core/event-listeners
- 6: https://github.com/vercel/ai/blob/08cdf6ae/content/docs/03-ai-sdk-core/05-generating-text.mdx
- 7: feat(ui): add finishReason in useChat onFinish callback vercel/ai#9857
- 8: https://ai-sdk.dev/docs/ai-sdk-core/lifecycle-callbacks
- 9: https://ai-sdk.dev/docs/ai-sdk-core/error-handling
- 10: https://github.com/vercel/ai/blob/83877a1e/content/docs/03-ai-sdk-core/50-error-handling.mdx
- 11:
onFinishis never called in streamText args vercel/ai#1841
🌐 Web query:
Vercel AI SDK onAbort error abort disconnect stream part error disconnect fields documentation
💡 Result:
In the Vercel AI SDK, the onAbort callback and stream abort handling are designed to manage server-side cleanup when a stream is canceled, typically via an AbortSignal [1][2][3]. Key concepts and documentation regarding stream aborts include: 1. The onAbort Callback: When a stream (e.g., created via streamText) is aborted, the onAbort callback is triggered instead of onEnd [1][3]. This allows you to perform cleanup tasks like updating UI states or persisting partial results [1][2]. It receives information about the completed steps before the abort [1][4]. 2. Handling Abort in Streams: You can also handle abort events directly by processing the stream chunks, where an abort chunk may be present [1][3][5]. 3. UI Message Streams (toUIMessageStream): For UI-level streaming responses, the onEnd callback receives an isAborted parameter [2][5]. To ensure this callback executes during an abort, you must pass consumeStream to the createUIMessageStreamResponse configuration [6][5]. Without this, the response may terminate before onEnd can be triggered [6]. 4. Distinction Between Abort and Disconnect: In setups using resumable streams (e.g., useChat with resume: true), a client-side abort (like closing a tab or calling stop) is treated as a "disconnect" rather than a request to cancel the underlying server-side generation [7][8]. To support explicit user cancellation in these cases, you should implement a dedicated stop endpoint that manually cancels the background work [7][8]. For detailed implementation and specific code patterns, refer to the official AI SDK documentation on Error Handling [1], Advanced: Stopping Streams [2], and the specific troubleshooting guide for stream abort handling [6]. Note that under certain conditions, such as very rapid abort signals, some users have reported the system triggering an onError path; ensure your error and abort handling logic is robust [9].
Citations:
- 1: https://ai-sdk.dev/v5/docs/ai-sdk-core/error-handling
- 2: https://ai-sdk.dev/v7/docs/advanced/stopping-streams
- 3: https://ai-sdk.dev/docs/ai-sdk-core/error-handling
- 4: https://ai-play.vercel.app/docs/ai-sdk-core/error-handling
- 5: https://ai-sdk.dev/docs/advanced/stopping-streams
- 6: https://ai-sdk.dev/docs/troubleshooting/stream-abort-handling
- 7: https://ai-sdk.dev/v7/docs/troubleshooting/abort-breaks-resumable-streams
- 8: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-resume-streams
- 9: bug: short abort signals in
streamText()triggeronError()instead ofonAbort()vercel/ai#8088
🌐 Web query:
Vercel AI SDK streamText response createUIMessageStreamResponse onEnd isAborted no error flags documentation
💡 Result:
When using createUIMessageStreamResponse in the Vercel AI SDK, the onEnd callback may fail to execute when a stream is aborted because the abort signal immediately terminates the response before the callback can be triggered [1][2]. This behavior can result in missing cleanup operations or logs [1]. To ensure that the onEnd callback is executed even when the stream is aborted, you must explicitly pass consumeStream from the ai package to the consumeSseStream property within the createUIMessageStreamResponse configuration [1][2][3]. Example implementation: import { consumeStream, createUIMessageStreamResponse, toUIMessageStream, streamText } from 'ai'; export async function POST(req: Request) { const { messages } = await req.json; const result = streamText({ //... model configuration abortSignal: req.signal, }); return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, onEnd: async ({ isAborted }) => { if (isAborted) { console.log('Stream was aborted'); } else { console.log('Stream completed normally'); } }, }), consumeSseStream: consumeStream, // Enables onEnd/onFinish to trigger on abort }); } When configured this way, the isAborted flag in the onEnd callback will correctly reflect the stream's status [2][4]. Without this configuration, the SDK does not inherently force the execution of onEnd upon abortion, as the underlying response connection is severed prematurely [1].
Citations:
- 1: https://ai-sdk.dev/docs/troubleshooting/stream-abort-handling
- 2: https://ai-sdk.dev/docs/advanced/stopping-streams
- 3:
onFinishfromcreateUIMessageStreamnot called when aborted vercel/ai#7469 - 4: https://ai-sdk.dev/v7/docs/advanced/stopping-streams
🌐 Web query:
Vercel AI SDK 3.0.8 onFinishStream return value fields abort error disconnect
💡 Result:
In the Vercel AI SDK, including versions around 3.0.8, the onFinish callback behavior regarding aborts, disconnects, and errors has evolved to provide better status tracking [1][2]. If you are using the useChat hook, recent updates have introduced isAbort, isDisconnect, and isError as fields returned in the onFinish callback to help you distinguish between normal completion and abnormal termination [1][2]. If these fields appear to be missing, ensure your dependencies (specifically @ai-sdk/react and ai) are updated to the latest versions, as older versions may not expose these properties [2]. For server-side stream handling, the behavior is as follows: 1. Handling Aborts: By default, onFinish or onEnd callbacks might not execute when a stream is aborted because the abort signal terminates the response immediately [3][4][5]. To ensure these callbacks execute even during an abort (e.g., to perform cleanup or save partial messages), you should configure your response handler—such as createUIMessageStreamResponse—with consumeSseStream: consumeStream [3][4]. 2. Distinguishing Aborts: In server-side utilities like toUIMessageStream, the onEnd callback provides an isAborted flag [3][4]. Using consumeStream allows this callback to trigger during an abort, allowing you to run custom logic based on whether the stream completed or was interrupted [3][4]. 3. Troubleshooting Consistency: If you encounter cases where isAborted is false even when an abort occurred, this is often due to inconsistency between different utility functions (like createUIMessageStream vs toUIMessageStream) [6]. Additionally, note that onAbort is a separate callback explicitly intended for handling stream cancellation, while onFinish is primarily reserved for normal completions [4][7]. If you are experiencing issues where onFinish is not triggering as expected, verify that you are not relying solely on onFinish for cleanup, and consider adding an onAbort callback for specific abort-related logic [4][7]. Ensure your environment is using compatible, up-to-date SDK packages to avoid discrepancies in callback signatures [2].
Citations:
- 1: feat(ui): sent isAbort, isDisconnect, isError in useChat onFinish callback vercel/ai#8364
- 2: Bug: onFinish only returns message, not messages vercel/ai#9307
- 3: https://ai-sdk.dev/docs/troubleshooting/stream-abort-handling
- 4: https://ai-sdk.dev/docs/advanced/stopping-streams
- 5:
onFinishintoUIMessageStreamshould be called on abort with the currently available partial ui message vercel/ai#7900 - 6:
isAbortedisfalsein abortedcreateUIMessageStream()'sonFinishcallback vercel/ai#8650 - 7: https://ai-sdk.dev/v5/docs/ai-sdk-core/error-handling
Tie fork bootstrap cleanup to durable child records.
persistEvent only enqueues durable records; drainPersist.sessionId) waits for those queued writes. Clear the bootstrap from a client streaming callback creates a race where a reload can send a last-message-only request without the queued first turn records, or replay a partially persisted turn that was drained on the first client connection. Use a server-acknowledged durable-write signal or reconcile records with a stable first-turn idempotency key before clearing the bootstrap.
| Add a logical transcript endpoint: | ||
|
|
||
| ```http | ||
| POST /sessions/transcript/query | ||
| ``` | ||
|
|
||
| Request: | ||
|
|
||
| ```json | ||
| { | ||
| "session_id": "session-B" | ||
| } | ||
| ``` | ||
|
|
||
| Response: | ||
|
|
||
| ```json | ||
| { | ||
| "session_id": "session-B", | ||
| "count": 42, | ||
| "records": [ | ||
| {"session_id": "session-A", "turn_id": "A1", "record_index": 0}, | ||
| {"session_id": "session-A", "turn_id": "A2", "record_index": 0}, | ||
| {"session_id": "session-B", "turn_id": "B1", "record_index": 0} | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| Existing `SessionRecord.session_id` already identifies provenance. The response does not rewrite | ||
| source IDs or add a misleading `inherited` field. A consumer can compare each record's session ID | ||
| with the requested session ID. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository files matching api-design:"
fd -i 'api-design\.md$|session-rewind|session' docs . | sed -n '1,120p'
echo
echo "Target file excerpt:"
if [ -f docs/design/session-rewind-branching/api-design.md ]; then
nl -ba docs/design/session-rewind-branching/api-design.md | sed -n '130,205p'
else
echo "target file not found"
fi
echo
echo "Search for transcript/query permissions/auth mentions:"
rg -n "transcript/query|/sessions/transcript|authorize|authorization|access|permissions|lineage|ancestor|parent|session_id|SessionRecord" . -g '*.md' -g '*.ts' -g '*.py' -g '*.go' -g '*.java' | sed -n '1,220p'Repository: Agenta-AI/agenta
Length of output: 7759
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target file excerpt using awk:"
awk '{printf "%6d\t%s\n", NR, $0}' docs/design/session-rewind-branching/api-design.md | sed -n '130,210p'
echo
echo "Search for transcript/query endpoint and authorization text:"
rg -n "transcript/query|/sessions/transcript|authorize|authorization|access|permissions|lineage|ancestor|parent|SessionRecord|session_id" docs/design/session-rewind-branching/api-design.md docs -g '*.md' | sed -n '1,260p'Repository: Agenta-AI/agenta
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant line ranges in target design file:"
awk '{printf "%6d\t%s\n", NR, $0}' docs/design/session-rewind-branching/api-design.md | sed -n '1,170p'
awk '{printf "%6d\t%s\n", NR, $0}' docs/design/session-rewind-branching/api-design.md | sed -n '210,245p'
echo
echo "Focused search in target design and sessions docs:"
rg -n "authorize|authorization|access|permission|/sessions/transcript/query|transcript/query|SessionRecord|lineage" docs/design/session-rewind-branching/api-design.md docs/designs docs/designs/sessions docs/designs/advanced-auth --glob '*.md' | sed -n '1,220p'Repository: Agenta-AI/agenta
Length of output: 43148
Authorize the full lineage before returning logical transcript records.
/sessions/transcript/query returns records from ancestor sessions while the request only names the child session. Add an authorization rule that requires access to the child session and every ancestor used by the logical transcript.
| Durable fork flow: | ||
|
|
||
| 1. Count the complete visible turns before the selected UI turn. | ||
| 2. Call the fork endpoint. | ||
| 3. Add the returned child summary to local state and make it active. | ||
| 4. Optimistically show the retained local prefix while transcript hydration resolves. | ||
| 5. Send the edited or copied user prompt as the child's first turn. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== git status =="
git status --short
echo "== locate file =="
fd -a 'api-design.md|desig.*|design.*' . | sed 's#^\./##' | head -100
echo "== target excerpt =="
if [ -f docs/design/session-rewind-branching/api-design.md ]; then
nl -ba docs/design/session-rewind-branching/api-design.md | sed -n '220,275p'
else
echo "target file not found"
fi
echo "== relevant mentions =="
rg -n "inherited_turn_count|durable|reconstructMessages|visible turns|rewind|turn_count|selected turn|branching" docs/design -S || trueRepository: Agenta-AI/agenta
Length of output: 2879
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file lines =="
awk '{printf "%6d\t%s\n", NR, $0}' docs/design/session-rewind-branching/api-design.md | sed -n '220,275p'
echo "== API contract sections =="
awk '{printf "%6d\t%s\n", NR, $0}' docs/design/session-rewind-branching/api-design.md | sed -n '1,140p'
echo "== relevant mentions =="
grep -RnE "inherited_turn_count|visible turn|visible turns|reconstructMessages|durable|rewind|branch|turn_count|selected turn|cut-point|boundary|assistant" docs/design/session-rewind-branching/api-design.md || trueRepository: Agenta-AI/agenta
Length of output: 9633
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== code references for fork API fields and reconstructMessages =="
rg -n "inheritedTurnCount|inherited_turn_count|cutoff_record_id|cutoff_session_id|reconstructMessages|visible turn|visible.*turn|effective transcript|complete turns" -S .
echo "== files likely containing record/message reconstruction =="
rg -n "function reconstructMessages|const reconstructMessages|reconstructMessages:|class .*Record|interface .*Record|SessionRecord|message.*record|physical record|turn" -S --glob '!node_modules' --glob '!dist' --glob '!build' | head -200Repository: Agenta-AI/agenta
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== focused code references =="
rg -n "inheritedTurnCount|inherited_turn_count|cutoff_record_id|cutoff_session_id|replayHistory|rewindForkAtomFamily|reconstructMessages|visible.*turn|visible turn|complete turns" \
-S \
--glob '!docs/**' \
--glob '!node_modules/**' \
--glob '!dist/**' \
--glob '!build/**' \
--glob '!*.md' \
. || true
echo "== focused frontend/design references in docs/session-rewind-branching =="
rg -n "replayHistory|rewindForkAtomFamily|reconstructMessages|visible.*turn|visible turn|complete turns|selected UI turn" \
-S docs/design/session-rewind-branching || trueRepository: Agenta-AI/agenta
Length of output: 6783
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== services/runner/src/sessions/reconstruct.ts =="
awk '{printf "%6d\t%s\n", NR, $0}' services/runner/src/sessions/reconstruct.ts | sed -n '1,160p'
echo "== services/runner/src/sessions/reconstruct.ts tests =="
awk '{printf "%6d\t%s\n", NR, $0}' services/runner/tests/unit/session-reconstruct.test.ts | sed -n '1,170p'
echo "== services/runner/src/engines/sandbox_agent/reconstruct-history.ts relevant =="
awk '{printf "%6d\t%s\n", NR, $0}' services/runner/src/engines/sandbox_agent/reconstruct-history.ts | sed -n '95,130p'Repository: Agenta-AI/agenta
Length of output: 14973
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== docs/session-rewind-branching references for visible and selected turn flow =="
grep -RnE "visible turn|selected UI turn|selected turn|replayHistory|buildAgentRequest|prompt|last-message-only" docs/design/session-rewind-branching || true
echo "== repository focused search for selected turn/fork bootstrap =="
grep -RnE "rewindForkAtomFamily|buildAgentRequest|replayHistory|last-message-only|selected.*turn|turnId" web/oss services runner --glob '!*node_modules*' --glob '!*.md' || trueRepository: Agenta-AI/agenta
Length of output: 2486
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== web/oss focused sources for transcript/render/fork =="
rg -n "visible turn|last-message-only|replayHistory|buildAgentRequest|turnId|rewindForkAtomFamily|selected.*turn|rewrite|copy.*prompt|prompt" web/oss/services --glob '!node_modules/**' | head -200 || true
echo "== design docs all relevant files line counts =="
find docs/design/session-rewind-branching -maxdepth 2 -type f -print | sort | while read -r f; do
wc -l "$f"
done
echo "== session-rewind-branching all markdown references to selected / visible / inherited / first turn =="
rg -n "selected|visible|inherited|first turn|last-message-only|rewindForkAtomFamily|replayHistory|copy|edit|rerun|incomplete" docs/design/session-rewind-branching || trueRepository: Agenta-AI/agenta
Length of output: 13716
Use a durable selected boundary instead of visible turn counts.
inherited_turn_count is a durable cursor, but step 1 derives it from complete visible UI turns. reconstructMessages can fold multiple assistant records into one UI message, so a complete visible message is not a durable turn identity. If the user selects an assistant turn, including its preceding user turn will duplicate that request when step 5 submits the edited/copied prompt as the child’s first turn. Define a stable selected turn/boundary and explicit inclusion semantics for both user and assistant rewind.
| The durable flow removes `replayHistory` and the persisted bootstrap because the runner no | ||
| longer depends on a browser-supplied prefix. |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Keep pending action state separate from transcript replay.
The durable flow can remove replayHistory, but it cannot remove persisted action state solely because the runner can reconstruct inherited context. ForkBootstrap also stores edit/rerun mode and the restored draft. After reload between fork creation and the first send, lineage does not tell the frontend whether to restore the composer or auto-rerun. Keep a persisted pending-action record until the first child request succeeds.
| Add a composite database operation, behind a core DAO interface, that owns one SQLAlchemy session | ||
| and transaction. It inserts the child `session_streams` row, the `session_lineage` row, and any | ||
| `session_attachment_access` rows. Unique constraints make an identical target retry idempotent; a | ||
| conflicting target causes the entire transaction to roll back. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Implement idempotency beyond the unique constraint.
A unique constraint prevents duplicate rows. It does not make a repeated POST return the existing fork. The DAO must handle the conflict, compare source, target, and cutoff, and return the existing child only for an identical request. Return a conflict for a different lineage.
| Recommended default: add `session_attachment_access` rows that grant the child access to existing | ||
| parent-owned attachments without copying bytes or changing attachment IDs. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Bound attachment access to the fork snapshot.
Define whether the child can access only attachments available at fork time or also attachments added to the parent later. Also define whether the access row grants read, write, or delete access, and how revocation works. Without these rules, child access can expand beyond the selected parent state.
| Recommended default: reject a fork when the selected turn is incomplete. Decide whether a completed | ||
| prefix of a currently running parent may be forked safely. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Capture a stable snapshot when forking a running parent.
Rejecting incomplete turns does not define how validation and cutoff capture synchronize with append-only writes. Capture the completion state, effective-turn count, and physical record cutoff atomically, or retry when the parent advances. Otherwise the child can inherit partial or mismatched context.
| - Frontend and backend may deploy at different times. Removing `replayHistory` requires a capability | ||
| or rollout gate. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Specify the mixed-version rollout contract.
“Requires a capability or rollout gate” is not an executable compatibility rule. Define the capability name, behavior for old and new client/server combinations, default behavior when absent, and the removal condition for replayHistory.
| - Session turns live in the core database while records may live in the tracing database. Effective | ||
| transcript resolution needs a bounded two-store read rather than a relational cross-database join. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Define consistency semantics for the two-store read.
A bounded read from the core database and tracing database is not one atomic snapshot. Specify write ordering, visibility delay, retry behavior, and the response when lineage exists but records are not visible. Otherwise transcript resolution can omit records or violate the physical cutoff.
| - The package-level `@agenta/chat` rewind remains in-place and can reintroduce the original bug when | ||
| that surface becomes active. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Gate the package-level rewind path before activation.
If @agenta/chat becomes reachable, its in-place rewind contradicts the settled new-session behavior and can reintroduce the original bug. Update that path before activation, or enforce a route or feature gate that excludes it from the release.
Context
PR #5860 fixes rewind resurrection by moving the user onto a new session, but the implementation still leaves open questions about reload safety, cross-device durability, metadata, and future branch storage. Reviewers lacked one place that separated the immediate frontend fix from the longer-term backend model.
Changes
Adds a self-contained design workspace for session rewind and branching. It documents the user story, current frontend and runner behavior, known failure modes, and the requested changes before PR #5860 merges.
The durable proposal keeps records append-only. A child session stores lineage to its parent, an effective-turn count, and an indexed physical cutoff. A lineage-aware transcript read reconstructs inherited context without copying parent records or adding frontend request fan-out. Separate documents cover API contracts, frontend state, attachment access, migrations, query counts, storage growth, and rollout phases.
Tests / notes
git diff --check -- docs/design/session-rewind-branchingpasses.docs/design/session-rewind-branching/README.mdfor the reading order.