feat(cli): add fork-session resume flag#4159
Conversation
📋 Review SummaryThis PR adds a 🔍 General Feedback
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — DeepSeek/deepseek-v4-pro via Qwen Code /review
| if (argv.forkSession && sessionId) { | ||
| const sourceSessionId = sessionId; | ||
| const forkedSessionId = randomUUID(); | ||
| await sessionService.forkSession(sourceSessionId, forkedSessionId); |
There was a problem hiding this comment.
[Suggestion] forkSession() 调用缺少 try-catch 错误处理。若 fork 失败(如源会话属于不同项目),会抛出原始异常堆栈而非用户友好的错误消息,与 loadCliConfig 中其他错误处理风格不一致。
| await sessionService.forkSession(sourceSessionId, forkedSessionId); | |
| if (argv.forkSession && sessionId) { | |
| const sourceSessionId = sessionId; | |
| const forkedSessionId = randomUUID(); | |
| try { | |
| await sessionService.forkSession(sourceSessionId, forkedSessionId); | |
| } catch (err) { | |
| writeStderrLine( | |
| `Failed to fork session ${sourceSessionId}: ${err instanceof Error ? err.message : String(err)}`, | |
| ); | |
| process.exit(1); | |
| } | |
| sessionId = forkedSessionId; | |
| sessionData = await sessionService.loadSession(forkedSessionId); | |
| if (!sessionData) { | |
| writeStderrLine(`Failed to load forked session ${forkedSessionId}.`); | |
| process.exit(1); | |
| } | |
| } |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
# Conflicts: # packages/cli/src/config/config.test.ts # packages/cli/src/config/config.ts
| process.exit(1); | ||
| } | ||
| } | ||
| } else if (argv.sandboxSessionId) { |
There was a problem hiding this comment.
[Suggestion] --sandbox-session-id is accepted from any CLI invocation — hidden: true only hides it from --help. The branch sets sessionId = argv.sandboxSessionId without the sessionService.sessionExists() collision check that --session-id performs just below, and without requiring process.env['SANDBOX'] (the codebase already gates sandbox-child behavior on this env var at gemini.tsx:462).
Consider adding a guard: reject the flag when process.env['SANDBOX'] is not set, or at minimum document why the collision guard is intentionally skipped here (the parent process already owns this UUID, but that invariant is not enforced in code).
| } else if (argv.sandboxSessionId) { | |
| } else if (argv.sandboxSessionId) { | |
| // Sandbox relaunch handoff: the parent process already owns this UUID. | |
| // Reject direct invocations (not inside the sandbox) to match the | |
| // documented "internal" semantics. | |
| if (!process.env['SANDBOX']) { | |
| writeStderrLine( | |
| '--sandbox-session-id is for internal sandbox use only.', | |
| ); | |
| process.exit(1); | |
| } | |
| sessionId = argv.sandboxSessionId; |
— claude-opus-4-7 via Qwen Code /review
本地真实场景测试报告(tmux)Head: Fork 链验证3 个连续会话操作,构成完整 fork 链: 每个 forked jsonl 的第一条记录都含 tmux 交互 TUI 验证启动: cd /tmp/pr4159-tui
node dist/cli.js --continue --fork-session
两条 Suggestion 都修了Suggestion 1 (09:22Z) —
|
| 项 | 结果 |
|---|---|
npx tsc --noEmit -p packages/cli |
0 错 |
npx tsc --noEmit -p packages/core |
0 错 |
config.test.ts |
213/213 pass + 2 skipped |
| GitHub CI | 全绿(Lint, Test ubuntu/macos/windows, CodeQL) |
--help 显示新 flag |
✅ "Create a new forked session from the resumed session. Must be used with --resume or --continue." |
LGTM ✅
wenshao
left a comment
There was a problem hiding this comment.
已发布详细测试报告(见 issue comment)。2 个 Suggestion 都修了,tmux 真测 fork 链 + forkedFrom 元数据 + 模型从原 context 答出 purple;CI 全绿,tsc 0 错,213/213 单测过。LGTM ✅
wenshao
left a comment
There was a problem hiding this comment.
[Critical] --continue --fork-session 成功路径缺少测试。当前仅覆盖了 loadLastSession() 返回空时的错误分支,未测试存在已保存会话时 fork 成功的正常路径。建议添加与 should fork and load a new session when --resume is combined with --fork-session 对等的 --continue 版本。
[Suggestion] forkSession 成功后 loadSession(forkedSessionId) 返回 undefined 的错误处理分支无测试覆盖。
[Suggestion] fork 成功测试仅验证了函数调用关系,未断言 Config 对象实际填充了 fork 后的会话数据,建议增加对 config 返回值的断言。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| } | ||
| if ( | ||
| argv['sandboxSessionId'] && | ||
| (argv['sessionId'] || argv['continue'] || argv['resume']) |
There was a problem hiding this comment.
[Suggestion] hasResume 变量在第 921 行引入用于正确识别 picker 模式(--resume 无参产生 ''),但此处 sandbox-session-id 互斥检查仍直接使用 argv['resume'](falsy 值),与上方 L922/925 的 hasResume 用法不一致。
| (argv['sessionId'] || argv['continue'] || argv['resume']) | |
| if ( | |
| argv['sandboxSessionId'] && | |
| (argv['sessionId'] || argv['continue'] || hasResume) | |
| ) { |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| try { | ||
| await sessionService.forkSession(sourceSessionId, forkedSessionId); | ||
| } catch (err) { | ||
| writeStderrLine( |
There was a problem hiding this comment.
[Suggestion] forkSession 失败时 err.message 可能包含原生 Node.js 错误中的绝对文件系统路径(如 ENOENT: no such file or directory, open '/Users/.../session.jsonl'),直接写入 stderr 会泄露内部目录结构。建议对错误消息做路径脱敏,或仅输出不含路径的通用失败提示。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| expect(mockSessionServiceInstance.forkSession).toHaveBeenCalledWith( | ||
| sourceSessionId, | ||
| config.getSessionId(), | ||
| ); |
There was a problem hiding this comment.
[Suggestion] 该断言读取 mock 内部数组 mock.calls[0]?.[1] 来验证 config.getSessionId(),与上方 L936 已断言 forkSession 以 (sourceSessionId, config.getSessionId()) 调用构成同义反复。建议改为更直接、有意义的断言:
| ); | |
| expect(config.getSessionId()).not.toBe(sourceSessionId); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| ); | ||
| }); | ||
|
|
||
| it('should explain when --fork-session fails to copy the source session', async () => { |
There was a problem hiding this comment.
[Suggestion] 此测试(以及下方 L976、L1007 两个测试)创建了 vi.spyOn(process, 'exit') 但未调用 mockRestore(),与文件中另外 16 处使用 mockRestore() 的模式不一致。虽然 afterEach 中 vi.restoreAllMocks() 会兜底,建议保持一致:在每个测试的 expect(mockExit).toHaveBeenCalledWith(1) 之后添加 mockExit.mockRestore();。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| process.exit(1); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
[Suggestion] sessionData 在 L1568 经 loadSession 加载源会话数据,在 fork block 中又被 loadSession(forkedSessionId) 覆盖。无注释说明源 sessionData 在 fork 路径下会被丢弃。未来若有人在 resume 加载和 fork 之间插入对 sessionData 的修改,该修改将在 fork 路径下静默丢失。建议添加注释或使用独立变量名(如 forkedSessionData)。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
Summary
--fork-sessionCLI flag for resuming into a new session--resume ... --fork-sessionand--continue --fork-sessionthroughSessionService.forkSession(...)--resume --fork-session, and forked resume loadingCloses #4158
Test plan
npm test -- --run src/config/config.test.tsnpm run lint -- src/config/config.ts src/config/config.test.tsNotes
npm run typecheckcurrently fails on unrelated existing project-wide type errors (for example missing core exports and channel package types). This change does not addforkSession/config-test-related type errors.