✨ feat(vscode): 移交包不带子代理对话
This commit is contained in:
1 parent
a5ea0ddb57
commit
c86939285d
6 files changed
+33
-15
No files matched your search
@@ -81,7 +81,7 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有)
|
||||
2. **worktree 守卫**(有 `worktreePath` 且目录存在时):`git status --porcelain` 必须为空;`git fetch origin <branch>` 后 `HEAD` 必须等于 `origin/<branch>`。否则 toast「有未提交/未推送改动」并终止,什么都不改。
|
||||
3. **会话大小**(关终端之前):统计将打进包的 jsonl、会话目录和 codex rollout。原始总字节不超过 Gitea 附件上限 100MB 则通过。超过则先压缩,压缩产出一旦超过 100MB 就停,toast 写明哪个会话多大,不关终端、不上传、不改指派。正式打包再卡一次同一上限;那次失败发生在关终端之后,按打包失败处理。
|
||||
4. 关闭该工单全部终端(`terminalOrigin` 同 issueNumber + 名称匹配 `issue-N-冲突解决`),等 600ms 让 jsonl 刷盘。
|
||||
5. 在 scratch 临时目录收集会话包:同一卷上用硬链接收文件,跨卷才复制。每个 sid 在 `~/.claude/projects/*/` 里找 `<sid>.jsonl`(多份取最新 mtime),连同 `<sid>/` 一起拷入 `claude/`;`reviewSessionId` 在 `~/.codex/sessions` 里按文件名尾部 uuid 找到 rollout 拷入 `codex/<relPath>`。找不到的会话不进清单,只 log。写 `handoff.json`,`tar -czf`。
|
||||
5. 在 scratch 临时目录收集会话包:同一卷上用硬链接收文件,跨卷才复制。每个 sid 在 `~/.claude/projects/*/` 里找 `<sid>.jsonl`(多份取最新 mtime),连同 `<sid>/` 一起拷入 `claude/`(跳过 `subagents/`,子代理对话不移交);`reviewSessionId` 在 `~/.codex/sessions` 里按文件名尾部 uuid 找到 rollout 拷入 `codex/<relPath>`。找不到的会话不进清单,只 log。写 `handoff.json`,`tar -czf`。
|
||||
6. 若工单已有同名旧附件(重复移交)先删。`POST …/assets?name=spx-handoff-issue-N.tgz` 上传。
|
||||
7. `mergeIssueState(issueNumber, { handoffAttachmentId, handoffFrom: me })`。
|
||||
8. `updateIssueAssignees(… [to])`。
|
||||
|
||||
@@ -31,9 +31,11 @@ function senderRoots(): { projectsRoot: string, codexRoot: string } {
|
||||
const wt = path.join(projectsRoot, '-Users-chw-wt-x')
|
||||
mkdirSync(proj, { recursive: true })
|
||||
mkdirSync(path.join(wt, IMPL, 'subagents'), { recursive: true })
|
||||
mkdirSync(path.join(wt, IMPL, 'tool-results'), { recursive: true })
|
||||
writeFileSync(path.join(proj, `${BRAIN}.jsonl`), 'brain\n')
|
||||
writeFileSync(path.join(wt, `${IMPL}.jsonl`), 'impl\n')
|
||||
writeFileSync(path.join(wt, IMPL, 'subagents', 'agent-1.jsonl'), '{}\n')
|
||||
writeFileSync(path.join(wt, IMPL, 'tool-results', 'out.txt'), 'tool\n')
|
||||
const day = path.join(codexRoot, '2026', '08', '26')
|
||||
mkdirSync(day, { recursive: true })
|
||||
writeFileSync(path.join(day, `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`), 'review\n')
|
||||
@@ -56,7 +58,8 @@ describe('stageHandoffBundle', () => {
|
||||
expect(manifest.sessions.testSessionId).toBe('missing-0000-4000-8000-000000000000')
|
||||
expect(manifest.createdAt).toBe('2026-08-26T00:00:00.000Z')
|
||||
expect(existsSync(path.join(staging, 'claude', `${IMPL}.jsonl`))).toBe(true)
|
||||
expect(existsSync(path.join(staging, 'claude', IMPL, 'subagents', 'agent-1.jsonl'))).toBe(true)
|
||||
expect(existsSync(path.join(staging, 'claude', IMPL, 'subagents', 'agent-1.jsonl'))).toBe(false)
|
||||
expect(existsSync(path.join(staging, 'claude', IMPL, 'tool-results', 'out.txt'))).toBe(true)
|
||||
expect(existsSync(path.join(staging, 'codex', manifest.codex[0].relPath))).toBe(true)
|
||||
expect(parseHandoffManifest(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8'), 7)).toEqual(manifest)
|
||||
})
|
||||
@@ -92,7 +95,8 @@ describe('pack → unpack → install', () => {
|
||||
await installHandoffBundle(extracted, parsed, { projectsRoot, codexRoot, workspaceProjectsDir, worktreeProjectsDir })
|
||||
expect(readFileSync(path.join(workspaceProjectsDir, `${BRAIN}.jsonl`), 'utf8')).toBe('brain\n')
|
||||
expect(readFileSync(path.join(worktreeProjectsDir, `${IMPL}.jsonl`), 'utf8')).toBe('impl\n')
|
||||
expect(existsSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'))).toBe(true)
|
||||
expect(existsSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'))).toBe(false)
|
||||
expect(readFileSync(path.join(worktreeProjectsDir, IMPL, 'tool-results', 'out.txt'), 'utf8')).toBe('tool\n')
|
||||
expect(existsSync(path.join(projectsRoot, '-stale', `${IMPL}.jsonl`))).toBe(false)
|
||||
expect(readFileSync(path.join(codexRoot, parsed.codex[0].relPath), 'utf8')).toBe('review\n')
|
||||
})
|
||||
@@ -114,7 +118,7 @@ describe('pack → unpack → install', () => {
|
||||
// sender 会话文件里嵌入发送方的 worktree 路径(plain / file:// 两种形式)
|
||||
const wt = path.join(roots.projectsRoot, '-Users-chw-wt-x')
|
||||
writeFileSync(path.join(wt, `${IMPL}.jsonl`), `{"cwd":"${WORKTREE_FROM}"}\n{"cwd":"file://${WORKTREE_FROM}"}\n`)
|
||||
writeFileSync(path.join(wt, IMPL, 'subagents', 'agent-1.jsonl'), `{"cwd":"${WORKTREE_FROM}"}\n`)
|
||||
writeFileSync(path.join(wt, IMPL, 'tool-results', 'out.txt'), `{"cwd":"${WORKTREE_FROM}"}\n`)
|
||||
const day = path.join(roots.codexRoot, '2026', '08', '26')
|
||||
writeFileSync(path.join(day, `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`), `{"cwd":"${WORKTREE_FROM}"}\n{"cwd":"file://${WORKTREE_FROM}"}\n`)
|
||||
|
||||
@@ -140,9 +144,9 @@ describe('pack → unpack → install', () => {
|
||||
})
|
||||
|
||||
const installedJsonl = readFileSync(path.join(worktreeProjectsDir, `${IMPL}.jsonl`), 'utf8')
|
||||
const installedSubagent = readFileSync(path.join(worktreeProjectsDir, IMPL, 'subagents', 'agent-1.jsonl'), 'utf8')
|
||||
const installedTool = readFileSync(path.join(worktreeProjectsDir, IMPL, 'tool-results', 'out.txt'), 'utf8')
|
||||
const installedRollout = readFileSync(path.join(codexRoot, manifest.codex[0].relPath), 'utf8')
|
||||
for (const content of [installedJsonl, installedSubagent, installedRollout]) {
|
||||
for (const content of [installedJsonl, installedTool, installedRollout]) {
|
||||
expect(content).toContain(WORKTREE_TO)
|
||||
expect(content).not.toContain(WORKTREE_FROM)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { HandoffManifest, HandoffProfiles, HandoffSessions } from './handoffManifest'
|
||||
import type { ClaudeSessionFiles } from './sessionBundle'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { CLAUDE_SESSION_FIELDS, HANDOFF_MANIFEST_FILE } from './handoffManifest'
|
||||
import {
|
||||
|
||||
codexRolloutRelPathFor,
|
||||
findClaudeSessionFiles,
|
||||
installClaudeSession,
|
||||
@@ -40,7 +42,12 @@ async function placeFile(src: string, dst: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 子代理对话。接手的人只恢复主会话,不打包。 */
|
||||
export const HANDOFF_SKIPPED_DIR = 'subagents'
|
||||
|
||||
async function placeTree(src: string, dst: string): Promise<void> {
|
||||
if (path.basename(src) === HANDOFF_SKIPPED_DIR)
|
||||
return
|
||||
const st = await fsp.lstat(src)
|
||||
if (st.isDirectory()) {
|
||||
await fsp.mkdir(dst, { recursive: true })
|
||||
@@ -51,6 +58,13 @@ async function placeTree(src: string, dst: string): Promise<void> {
|
||||
await placeFile(src, dst)
|
||||
}
|
||||
|
||||
export async function stageClaudeSessionFiles(files: ClaudeSessionFiles, dstDir: string): Promise<void> {
|
||||
await fsp.mkdir(dstDir, { recursive: true })
|
||||
await placeFile(files.jsonl, path.join(dstDir, path.basename(files.jsonl)))
|
||||
if (files.dir)
|
||||
await placeTree(files.dir, path.join(dstDir, path.basename(files.dir)))
|
||||
}
|
||||
|
||||
export async function stageHandoffBundle(
|
||||
src: HandoffSource,
|
||||
stagingDir: string,
|
||||
@@ -70,9 +84,7 @@ export async function stageHandoffBundle(
|
||||
const files = await findClaudeSessionFiles(id, roots.projectsRoot)
|
||||
if (!files)
|
||||
continue
|
||||
await placeFile(files.jsonl, path.join(claudeDir, path.basename(files.jsonl)))
|
||||
if (files.dir)
|
||||
await placeTree(files.dir, path.join(claudeDir, path.basename(files.dir)))
|
||||
await stageClaudeSessionFiles(files, claudeDir)
|
||||
claude.push({ id, kind })
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('measureHandoffSources', () => {
|
||||
const items = await measureHandoffSources(src, roots)
|
||||
expect(items).toEqual([
|
||||
{ role: '头脑风暴', bytes: Buffer.byteLength('brain\n') },
|
||||
{ role: '实施', bytes: Buffer.byteLength('impl\n') + Buffer.byteLength('{}\n') },
|
||||
{ role: '实施', bytes: Buffer.byteLength('impl\n') },
|
||||
{ role: '审查', bytes: Buffer.byteLength('review\n') },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { promises as fsp } from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { formatByteSize, HANDOFF_ATTACHMENT_MAX_BYTES, HandoffArchiveTooLargeError, packHandoff } from './handoffArchive'
|
||||
import { stageHandoffBundle } from './handoffBundle'
|
||||
import { HANDOFF_SKIPPED_DIR, stageHandoffBundle } from './handoffBundle'
|
||||
import { CLAUDE_SESSION_FIELDS } from './handoffManifest'
|
||||
import { codexRolloutRelPathFor, findClaudeSessionFiles } from './sessionBundle'
|
||||
|
||||
@@ -45,8 +45,11 @@ async function treeBytes(abs: string): Promise<number> {
|
||||
if (!st.isDirectory())
|
||||
return st.size
|
||||
let total = 0
|
||||
for (const name of await fsp.readdir(abs))
|
||||
for (const name of await fsp.readdir(abs)) {
|
||||
if (name === HANDOFF_SKIPPED_DIR)
|
||||
continue
|
||||
total += await treeBytes(path.join(abs, name))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { packHandoff, unpackHandoff } from '../../cc/handoffArchive'
|
||||
import { stageClaudeSessionFiles } from '../../cc/handoffBundle'
|
||||
import { HANDOFF_MANIFEST_FILE } from '../../cc/handoffManifest'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import {
|
||||
claudeProjectsRoot,
|
||||
copyClaudeSessionFiles,
|
||||
findClaudeSessionFiles,
|
||||
installClaudeSession,
|
||||
rewritePathInFile,
|
||||
@@ -125,8 +125,7 @@ export async function handleSessionHandoffStart(panel: KanbanWebviewPanel, sessi
|
||||
let mailboxIndex: number
|
||||
try {
|
||||
const claudeDir = path.join(staging, 'claude')
|
||||
await fsp.mkdir(claudeDir, { recursive: true })
|
||||
await copyClaudeSessionFiles(files, claudeDir)
|
||||
await stageClaudeSessionFiles(files, claudeDir)
|
||||
await fsp.writeFile(path.join(staging, HANDOFF_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
mailboxIndex = await ensureMailboxIssue(ctx)
|
||||
const archive = path.join(outDir, attachmentName)
|
||||
|
||||
Reference in new issue
Block a user