fix: restore project handoff records

This commit is contained in:
Yoilun
2026-05-26 12:00:31 +08:00
parent cb46d5bc04
commit e08768d8fd
2 changed files with 182 additions and 6 deletions

View File

@@ -254,12 +254,14 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
...threadIds,
...runtime.threads.filter((thread) => threadBelongsToProject(thread, targetPath)).map((thread) => thread.id),
])
const projectMainThreadIds = new Set(
runtime.threads.filter((thread) => projectThreadIds.has(thread.id) && isMainThread(thread)).map((thread) => thread.id),
)
const agentIdsWithProjectMainEdge = buildAgentIdsWithProjectMainEdge(runtime.edges, threadIds, projectMainThreadIds)
const threads = runtime.threads.filter((thread) => threadIds.has(thread.id))
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId) || projectThreadIds.has(goal.threadId))
const edges = runtime.edges.filter((edge) =>
(threadIds.has(edge.fromThreadId) || threadIds.has(edge.toThreadId)) &&
projectThreadIds.has(edge.fromThreadId) &&
projectThreadIds.has(edge.toThreadId),
edgeBelongsToProjectFlow(edge, { threadIds, projectThreadIds, threadByID, targetPath, agentIdsWithProjectMainEdge }),
)
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
const phaseGroups = buildPhaseGroups(agents)
@@ -286,6 +288,55 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
}
}
function buildAgentIdsWithProjectMainEdge(edges, threadIds, projectMainThreadIds) {
const agentIds = new Set()
for (const edge of edges) {
if (projectMainThreadIds.has(edge.fromThreadId) && threadIds.has(edge.toThreadId)) {
agentIds.add(edge.toThreadId)
}
if (projectMainThreadIds.has(edge.toThreadId) && threadIds.has(edge.fromThreadId)) {
agentIds.add(edge.fromThreadId)
}
}
return agentIds
}
function edgeBelongsToProjectFlow(edge, { threadIds, projectThreadIds, threadByID, targetPath, agentIdsWithProjectMainEdge }) {
const fromIsAgent = threadIds.has(edge.fromThreadId)
const toIsAgent = threadIds.has(edge.toThreadId)
const fromIsProjectThread = projectThreadIds.has(edge.fromThreadId)
const toIsProjectThread = projectThreadIds.has(edge.toThreadId)
const fromThread = threadByID.get(edge.fromThreadId)
const toThread = threadByID.get(edge.toThreadId)
const fromIsMain = mainThreadCanJoinProjectFlow(fromThread, targetPath, agentIdsWithProjectMainEdge, edge.toThreadId)
const toIsMain = mainThreadCanJoinProjectFlow(toThread, targetPath, agentIdsWithProjectMainEdge, edge.fromThreadId)
if (fromIsAgent && toIsAgent) {
return true
}
if (fromIsAgent && (toIsProjectThread || toIsMain)) {
return true
}
if (toIsAgent && (fromIsProjectThread || fromIsMain)) {
return true
}
return false
}
function mainThreadCanJoinProjectFlow(thread, targetPath, agentIdsWithProjectMainEdge, agentThreadId) {
if (!isMainThread(thread)) {
return false
}
if (threadBelongsToProject(thread, targetPath)) {
return true
}
if (agentIdsWithProjectMainEdge.has(agentThreadId)) {
return false
}
const cwd = normalizePath(thread?.cwd)
return Boolean(cwd && targetPath.startsWith(`${cwd}/`))
}
function normalizeProjectHandoff(edge, context) {
const source = normalizeSource(edge.source, context.source)
const status = normalizeSpawnStatus(edge.reason || edge.status) || 'unknown'
@@ -528,11 +579,11 @@ function enrichProjectWorkflow(agent, targetPath) {
function extractWorkflowBatchName(values) {
for (const value of values) {
const text = String(value || '')
const explicit = text.match(/工作流批次\s*[:]\s*([^\]\n]+)/)
const explicit = text.match(/工作流批次\s*[:]\s*([^\]`\n]+)/)
if (explicit) {
return explicit[1].trim()
return cleanExplicitWorkflowBatchName(explicit[1])
}
const version = text.match(/\b(v[0-9]+(?:\.[0-9]+)+)\s*([^\]\n,]*)/i)
const version = text.match(/\b(v[0-9]+(?:\.[0-9]+)+)\s*([^\]`\n,。:/、]*)/i)
if (version && /重构|升级|优化|修复|初始|搭建|发布/.test(version[2] || '')) {
return `${version[1]} ${version[2].trim()}`.trim()
}
@@ -540,6 +591,12 @@ function extractWorkflowBatchName(values) {
return ''
}
function cleanExplicitWorkflowBatchName(value) {
return String(value || '')
.replace(/\s*[\/、]\s*(?:优化阶段|阶段)\s*[0-9一二三四五六七八九十]+.*$/, '')
.trim()
}
function normalizeWorkflowBatchName(value) {
return String(value || '默认工作流').replace(/^(v[0-9]+(?:\.[0-9]+)+)([^\s].*)$/, '$1 $2').trim()
}

View File

@@ -320,6 +320,94 @@ test('bare subagent threads are not treated as main supervision threads', () =>
assert.equal(projectRuntime.handoffs[0].directionLabel, '子智能体交接')
})
test('keeps main thread handoffs to project agents even when the main thread has only ancestor cwd', () => {
const runtime = normalizeRuntime({
items: [
{
id: 'main',
cwd: '/Users/yoilun',
title: '我想要一个图形页面去管理 code 文件夹下的项目',
threadSource: 'user',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'reviewer',
cwd: '/Users/yoilun',
title: '请审查 `/Users/yoilun/Code/codex-agent-manager` 当前未提交改动。背景:用户要求新增批次切换。',
agentNickname: 'Mencius',
agentRole: '代码审查员',
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
],
edges: [
{ fromThreadId: 'main', toThreadId: 'reviewer', reason: 'closed' },
],
source: { kind: 'sqlite_readonly', confidence: 'high' },
})
const projectRuntime = filterRuntimeByProject(runtime, '/Users/yoilun/Code/codex-agent-manager')
assert.equal(projectRuntime.agents.length, 1)
assert.equal(projectRuntime.handoffs.length, 1)
assert.equal(projectRuntime.handoffs[0].directionLabel, '主线程派发')
assert.equal(projectRuntime.workflowBatches[0].handoffCount, 1)
assert.deepEqual(projectRuntime.workflowBatches[0].phases[0].handoffs.map((handoff) => handoff.to), [
'Mencius / 代码审查员',
])
})
test('keeps parent main handoff when another project main is unrelated to that agent', () => {
const runtime = normalizeRuntime({
items: [
{
id: 'project-main',
cwd: '/Users/yoilun',
title: '另一个明确提到 /Users/yoilun/Code/codex-agent-manager 的用户线程,但没有派发这个智能体',
threadSource: 'user',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'parent-main',
cwd: '/Users/yoilun',
title: '继续整个项目',
threadSource: 'user',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'reviewer',
cwd: '/Users/yoilun',
title: '请审查 `/Users/yoilun/Code/codex-agent-manager` 当前 bugfix。',
agentNickname: 'Hubble',
agentRole: '代码审查员',
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'other-agent',
cwd: '/Users/yoilun',
title: '阶段 2另一个 /Users/yoilun/Code/codex-agent-manager 智能体',
agentNickname: 'Noether',
agentRole: '后端架构师',
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
],
edges: [
{ fromThreadId: 'parent-main', toThreadId: 'reviewer', reason: 'open' },
{ fromThreadId: 'project-main', toThreadId: 'other-agent', reason: 'closed' },
],
source: { kind: 'sqlite_readonly', confidence: 'high' },
})
const projectRuntime = filterRuntimeByProject(runtime, '/Users/yoilun/Code/codex-agent-manager')
assert.deepEqual(projectRuntime.handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
'主线程 / 主智能体监管 -> Hubble / 代码审查员',
'主线程 / 主智能体监管 -> Noether / 后端架构师',
])
})
test('groups project flow by workflow batch and classifies current optimizations', () => {
const runtime = normalizeRuntime({
items: [
@@ -357,6 +445,33 @@ test('groups project flow by workflow batch and classifies current optimizations
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'review-prompt',
cwd: '/Users/yoilun',
title: '请审查 /Users/yoilun/Code/codex-agent-manager。背景将优化改造归类到 `v1.1 优化修复 / 优化阶段 1`。当前实现会给项目内 agent/handoff 增加工作流批次。',
agentNickname: 'Peirce',
agentRole: '代码审查员',
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'explicit-slash-batch',
cwd: '/Users/yoilun',
title: '[项目: /Users/yoilun/Code/codex-agent-manager] [工作流批次: v1.2 CI/CD 发布] [阶段: 阶段 9 发布验证]',
agentNickname: 'Curie',
agentRole: '发布经理',
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
{
id: 'explicit-phase-tail',
cwd: '/Users/yoilun',
title: '[项目: /Users/yoilun/Code/codex-agent-manager] [工作流批次: v1.1 优化修复 / 优化阶段 1] [阶段: 优化阶段 1]',
agentNickname: 'Turing',
agentRole: '前端开发者',
threadSource: 'subagent',
source: { kind: 'sqlite_table', confidence: 'high' },
},
],
edges: [
{ fromThreadId: 'main', toThreadId: 'initial', reason: 'closed' },
@@ -373,9 +488,13 @@ test('groups project flow by workflow batch and classifies current optimizations
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'optimization').phaseName, '优化阶段 1')
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'v2').workflowBatch, 'v2.0 重构升级')
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'v2').phaseName, '阶段 3')
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'review-prompt').workflowBatch, 'v1.1 优化修复')
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'explicit-slash-batch').workflowBatch, 'v1.2 CI/CD 发布')
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'explicit-phase-tail').workflowBatch, 'v1.1 优化修复')
assert.deepEqual(projectRuntime.workflowBatches.map((batch) => batch.name), [
'v1.0 初始搭建',
'v1.1 优化修复',
'v1.2 CI/CD 发布',
'v2.0 重构升级',
])
assert.deepEqual(projectRuntime.workflowBatches[1].phases.map((phase) => phase.name), ['优化阶段 1'])