Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
0a4603d
feat(web-shell): branch from completed assistant responses
Jul 30, 2026
bd86733
fix(session): preserve historical branch checkpoints
Aug 1, 2026
870af37
fix(web-shell): harden response session branching
Aug 1, 2026
f901538
Merge branch 'main' into feat/fork-from-one-conversation
water-in-stone Aug 1, 2026
4685801
chore: remove PR comment evaluation artifact
Aug 1, 2026
ed2e419
fix(web-shell): guard historical branch mutations
Aug 1, 2026
dc987bc
Merge branch 'main' into feat/fork-from-one-conversation
wenshao Aug 1, 2026
d7f583e
perf(core): index historical branch points during transcript scan
Aug 1, 2026
d56f473
Merge branch 'main' into feat/fork-from-one-conversation
yiliang114 Aug 1, 2026
e6babb7
fix(core): address review findings — dead code, boundary remap, promp…
qwen-code-ci-bot Aug 2, 2026
613719c
fix(core): address review findings — dead code, boundary remap, promp…
qwen-code-ci-bot Aug 2, 2026
a83b386
fix(core): address review findings — archived GC, subtype registratio…
Aug 2, 2026
ebe1a96
Merge branch 'main' into feat/fork-from-one-conversation
wenshao Aug 2, 2026
bc46b21
test: strengthen branch-point and fork coverage from review (#8274)
qwen-code-ci-bot Aug 2, 2026
1a44cff
fix: address round-4 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 2, 2026
ea2ac9f
fix: address round-5 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 2, 2026
f1fa51c
fix: address round-6 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 3, 2026
b1bf5ff
fix: address round-7 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 3, 2026
e5ae883
Merge branch 'main' into feat/fork-from-one-conversation
qwen-code-dev-bot Aug 3, 2026
9d01e89
Merge branch 'main' into feat/fork-from-one-conversation
wenshao Aug 3, 2026
f05a71d
Merge branch 'main' into feat/fork-from-one-conversation
wenshao Aug 4, 2026
54b7aee
Merge remote-tracking branch 'origin/main' into feat/fork-from-one-co…
qwen-code-ci-bot Aug 4, 2026
759dc6a
Merge branch 'main' into feat/fork-from-one-conversation
qwen-code-dev-bot Aug 4, 2026
5bb933b
fix(core): harden branch-point resolution against malformed transcrip…
qwen-code-dev-bot Aug 5, 2026
d9d3464
Merge remote-tracking branch 'origin/main' into feat/fork-from-one-co…
qwen-code-dev-bot Aug 5, 2026
9caf930
Merge remote-tracking branch 'origin/main' into feat/fork-from-one-co…
qwen-code-dev-bot Aug 5, 2026
8b6390c
fix: address round-8 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 5, 2026
44fee68
fix: address round-9 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 6, 2026
ad24121
test(core): pin branch GC isolation from throwing warning callbacks (…
qwen-code-ci-bot Aug 6, 2026
7c18941
Merge branch 'main' into feat/fork-from-one-conversation
water-in-stone Aug 6, 2026
3909065
Merge branch 'main' into feat/fork-from-one-conversation
wenshao Aug 6, 2026
8f7fe40
fix(acp-bridge): reject rewind at admission while a prompt is active …
qwen-code-ci-bot Aug 6, 2026
0259297
fix(web-shell): harden session branch publication
Aug 7, 2026
0929b21
Merge remote-tracking branch 'upstream/main' into feat/fork-from-one-…
Aug 7, 2026
1524703
Merge branch 'main' into feat/fork-from-one-conversation
qwen-code-dev-bot Aug 7, 2026
d5562bd
fix(web-shell): harden historical response branching
Aug 7, 2026
f5b7b43
Merge remote-tracking branch 'origin/feat/fork-from-one-conversation'…
Aug 7, 2026
2a0e1dc
fix: address round-11 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 7, 2026
32617db
Merge remote-tracking branch 'origin/main' into feat/fork-from-one-co…
qwen-code-ci-bot Aug 7, 2026
20bf878
fix: address round-12 review feedback for session branching (#8274)
qwen-code-ci-bot Aug 7, 2026
40c50aa
Merge origin/main into feat/fork-from-one-conversation
qwen-code-ci-bot Aug 8, 2026
26b96c7
Merge branch 'main' into feat/fork-from-one-conversation
qwen-code-dev-bot Aug 8, 2026
e3e8469
refactor(branching): remove branch-specific overdesign
Aug 9, 2026
1ddb7a3
Merge branch 'main' into feat/fork-from-one-conversation
qwen-code-dev-bot Aug 9, 2026
60494e3
Merge remote-tracking branch 'upstream/main' into feat/fork-from-one-…
Aug 9, 2026
288592c
Merge branch 'main' into feat/fork-from-any-conversation
Aug 10, 2026
c912094
chore(sdk): update browser bundle budget
Aug 10, 2026
585efa3
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 10, 2026
d30eabd
fix(branching): address review lifecycle gaps
Aug 10, 2026
2f7c22f
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 11, 2026
3dedbbf
refactor(session): simplify branching safeguards
Aug 11, 2026
07f354f
test(branching): simplify regression coverage
Aug 11, 2026
37443fe
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 12, 2026
93fd80a
fix(serve): handle branch busy admission
Aug 12, 2026
3b12f83
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 12, 2026
859a332
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 12, 2026
a30d619
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 12, 2026
7e1fdab
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 13, 2026
e76deb5
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 13, 2026
396de48
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 13, 2026
00ce316
fix(sdk): preserve v1 branch session contract
Aug 13, 2026
a3b7c27
fix(daemon): guard branching history mutations
Aug 13, 2026
452deb4
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 14, 2026
134925e
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 14, 2026
df26a9a
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 14, 2026
bec0d95
Merge remote-tracking branch 'upstream/main' into feat/fork-from-any-…
Aug 14, 2026
a99bc8a
fix(branching): preserve fork and checkpoint semantics
Aug 14, 2026
01d4822
Merge branch 'main' into feat/fork-from-any-conversation
Aug 15, 2026
602bb08
fix(branching): harden branch and rewind behavior
Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
962 changes: 962 additions & 0 deletions docs/design/web-shell/assistant-response-session-branching.md

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions docs/developers/daemon/09-event-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,16 @@ These events are workspace-keyed, not session-keyed. The session reducer treats

### Turn lifecycle / assistant pushes

| Type | Direction | Trigger | Key payload fields |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `prompt_cancelled` | S->C | Prompt was cancelled through explicit `cancelSession` route **or** originator SSE disconnect | Envelope stamps `originatorClientId` for the canceling client. This means "cancellation requested", not "cancellation confirmed". Peer subscribers learn that the prompt has ended. |
| `turn_complete` | S->C | A turn completed successfully | `sessionId, stopReason, promptId?`. `promptId` links to non-blocking prompt responses (`202`). The SDK matches SSE events to the originating prompt through it. |
| `turn_error` | S->C | A turn failed | `sessionId, message, code?, promptId?`; same `promptId` correlation mechanism. |
| `session_rewound` | S->C | `POST /session/:id/rewind` succeeded | `sessionId, promptId, targetTurnIndex, filesChanged[], filesFailed[], originatorClientId?` |
| `session_branched` | S->C | `POST /session/:id/branch` created a branch from an existing session | `sourceSessionId, newSessionId, displayName, originatorClientId?` |
| `followup_suggestion` | S->C | ACP child generated ghost-text follow-up suggestions after `end_turn`, forwarded over per-session SSE | `sessionId, suggestion, promptId`; wire only carries suggestions whose `getFilterReason()===null`. Clients render them as input-placeholder ghost text and invalidate them on next `sendPrompt`. |
| `user_shell_command` | S->C | User started a shell command through `POST /session/:id/shell`; fanned out to other subscribers in the same session | `sessionId, command, shellId, originatorClientId?`. There is no typed `DaemonXxxData` interface yet; `asKnownDaemonEvent` returns `undefined` and the UI normalizer parses it ad hoc. |
| `user_shell_result` | S->C | Result of the shell command above | `sessionId, shellId, exitCode, output, aborted`. Same ad hoc parsing note as `user_shell_command`. |
| Type | Direction | Trigger | Key payload fields |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt_cancelled` | S->C | Prompt was cancelled through explicit `cancelSession` route **or** originator SSE disconnect | Envelope stamps `originatorClientId` for the canceling client. This means "cancellation requested", not "cancellation confirmed". Peer subscribers learn that the prompt has ended. |
| `turn_complete` | S->C | A turn completed successfully | `sessionId, stopReason, promptId?, branchPoint?`. `promptId` links to non-blocking prompt responses (`202`). Eligible completed turns include `branchPoint: { assistantRecordUuid, checkpointUuid }`. |
| `turn_error` | S->C | A turn failed | `sessionId, message, code?, promptId?`; same `promptId` correlation mechanism. |
| `session_rewound` | S->C | `POST /session/:id/rewind` succeeded | `sessionId, promptId, targetTurnIndex, filesChanged[], filesFailed[], originatorClientId?` |
| `session_branched` | S->C | Legacy compatibility event; the current branch endpoint returns its result directly and does not publish this event | `sourceSessionId, newSessionId, displayName, originatorClientId?`. Readers retain support for older producers. |
| `followup_suggestion` | S->C | ACP child generated ghost-text follow-up suggestions after `end_turn`, forwarded over per-session SSE | `sessionId, suggestion, promptId`; wire only carries suggestions whose `getFilterReason()===null`. Clients render them as input-placeholder ghost text and invalidate them on next `sendPrompt`. |
| `user_shell_command` | S->C | User started a shell command through `POST /session/:id/shell`; fanned out to other subscribers in the same session | `sessionId, command, shellId, originatorClientId?`. There is no typed `DaemonXxxData` interface yet; `asKnownDaemonEvent` returns `undefined` and the UI normalizer parses it ad hoc. |
| `user_shell_result` | S->C | Result of the shell command above | `sessionId, shellId, exitCode, output, aborted`. Same ad hoc parsing note as `user_shell_command`. |

## Architecture

Expand Down
72 changes: 72 additions & 0 deletions integration-tests/cli/qwen-serve-streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,78 @@ describePOSIX('qwen serve — Last-Event-ID resume', () => {
}, 60_000);
});

describePOSIX('qwen serve — historical Assistant response branch', () => {
it('creates, opens, and continues a branch through the real daemon', async () => {
const source = await client.createOrAttachSession({
workspaceCwd: workspaceDir,
sessionScope: 'thread',
});
const first = await client.prompt(source.sessionId, {
prompt: [{ type: 'text', text: 'historical branch turn one' }],
});
expect(first.branchPoint).toBeDefined();
if (!first.branchPoint) return;

await client.prompt(source.sessionId, {
prompt: [{ type: 'text', text: 'historical branch turn two' }],
});
await client.prompt(source.sessionId, {
prompt: [{ type: 'text', text: 'historical branch turn three' }],
});

const branched = await client.branchSession(source.sessionId, {
atRecordId: first.branchPoint.checkpointUuid,
});
const branchBeforeContinue = await client.getSessionTranscriptPage(
branched.sessionId,
{ limit: 500 },
);
const branchBeforeText = JSON.stringify(branchBeforeContinue.events);
expect(branchBeforeText).toContain('historical branch turn one');
expect(branchBeforeText).not.toContain('historical branch turn two');
expect(branchBeforeText).not.toContain('historical branch turn three');

const sourceAfterBranch = await client.getSessionTranscriptPage(
source.sessionId,
{ limit: 500 },
);
const sourceText = JSON.stringify(sourceAfterBranch.events);
expect(sourceText).toContain('historical branch turn one');
expect(sourceText).toContain('historical branch turn two');
expect(sourceText).toContain('historical branch turn three');

const loadedBranch = await client.loadSession(branched.sessionId);
await client.prompt(
branched.sessionId,
{
prompt: [{ type: 'text', text: 'continue the historical branch' }],
},
Comment thread
water-in-stone marked this conversation as resolved.
undefined,
loadedBranch.clientId,
);
Comment thread
water-in-stone marked this conversation as resolved.
const branchAfterContinue = await client.getSessionTranscriptPage(
branched.sessionId,
{ limit: 500 },
);
expect(JSON.stringify(branchAfterContinue.events)).toContain(
'continue the historical branch',
);

// The source session must stay untouched by the fork's continuation.
const sourceAfterContinue = await client.getSessionTranscriptPage(
source.sessionId,
{ limit: 500 },
);
const sourceAfterContinueText = JSON.stringify(sourceAfterContinue.events);
expect(sourceAfterContinueText).not.toContain(
'continue the historical branch',
);
expect(sourceAfterContinueText).toContain('historical branch turn one');
expect(sourceAfterContinueText).toContain('historical branch turn two');
expect(sourceAfterContinueText).toContain('historical branch turn three');
}, 90_000);
});

describePOSIX('qwen serve — daemon Todo Stop Guard replay', () => {
it('continues after prompt admission without an SSE client and replays the bounded attempts', async () => {
const session = await client.createOrAttachSession({
Expand Down
Loading
Loading