fix: restore project handoff records
This commit is contained in:
@@ -254,12 +254,14 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
...threadIds,
|
...threadIds,
|
||||||
...runtime.threads.filter((thread) => threadBelongsToProject(thread, targetPath)).map((thread) => thread.id),
|
...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 threads = runtime.threads.filter((thread) => threadIds.has(thread.id))
|
||||||
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId) || projectThreadIds.has(goal.threadId))
|
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId) || projectThreadIds.has(goal.threadId))
|
||||||
const edges = runtime.edges.filter((edge) =>
|
const edges = runtime.edges.filter((edge) =>
|
||||||
(threadIds.has(edge.fromThreadId) || threadIds.has(edge.toThreadId)) &&
|
edgeBelongsToProjectFlow(edge, { threadIds, projectThreadIds, threadByID, targetPath, agentIdsWithProjectMainEdge }),
|
||||||
projectThreadIds.has(edge.fromThreadId) &&
|
|
||||||
projectThreadIds.has(edge.toThreadId),
|
|
||||||
)
|
)
|
||||||
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
||||||
const phaseGroups = buildPhaseGroups(agents)
|
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) {
|
function normalizeProjectHandoff(edge, context) {
|
||||||
const source = normalizeSource(edge.source, context.source)
|
const source = normalizeSource(edge.source, context.source)
|
||||||
const status = normalizeSpawnStatus(edge.reason || edge.status) || 'unknown'
|
const status = normalizeSpawnStatus(edge.reason || edge.status) || 'unknown'
|
||||||
@@ -528,11 +579,11 @@ function enrichProjectWorkflow(agent, targetPath) {
|
|||||||
function extractWorkflowBatchName(values) {
|
function extractWorkflowBatchName(values) {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
const text = String(value || '')
|
const text = String(value || '')
|
||||||
const explicit = text.match(/工作流批次\s*[::]\s*([^\]\n;]+)/)
|
const explicit = text.match(/工作流批次\s*[::]\s*([^\]`\n;]+)/)
|
||||||
if (explicit) {
|
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] || '')) {
|
if (version && /重构|升级|优化|修复|初始|搭建|发布/.test(version[2] || '')) {
|
||||||
return `${version[1]} ${version[2].trim()}`.trim()
|
return `${version[1]} ${version[2].trim()}`.trim()
|
||||||
}
|
}
|
||||||
@@ -540,6 +591,12 @@ function extractWorkflowBatchName(values) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanExplicitWorkflowBatchName(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/\s*[\/、]\s*(?:优化阶段|阶段)\s*[0-9一二三四五六七八九十]+.*$/, '')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeWorkflowBatchName(value) {
|
function normalizeWorkflowBatchName(value) {
|
||||||
return String(value || '默认工作流').replace(/^(v[0-9]+(?:\.[0-9]+)+)([^\s].*)$/, '$1 $2').trim()
|
return String(value || '默认工作流').replace(/^(v[0-9]+(?:\.[0-9]+)+)([^\s].*)$/, '$1 $2').trim()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,6 +320,94 @@ test('bare subagent threads are not treated as main supervision threads', () =>
|
|||||||
assert.equal(projectRuntime.handoffs[0].directionLabel, '子智能体交接')
|
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', () => {
|
test('groups project flow by workflow batch and classifies current optimizations', () => {
|
||||||
const runtime = normalizeRuntime({
|
const runtime = normalizeRuntime({
|
||||||
items: [
|
items: [
|
||||||
@@ -357,6 +445,33 @@ test('groups project flow by workflow batch and classifies current optimizations
|
|||||||
threadSource: 'subagent',
|
threadSource: 'subagent',
|
||||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
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: [
|
edges: [
|
||||||
{ fromThreadId: 'main', toThreadId: 'initial', reason: 'closed' },
|
{ 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 === 'optimization').phaseName, '优化阶段 1')
|
||||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'v2').workflowBatch, 'v2.0 重构升级')
|
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 === '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), [
|
assert.deepEqual(projectRuntime.workflowBatches.map((batch) => batch.name), [
|
||||||
'v1.0 初始搭建',
|
'v1.0 初始搭建',
|
||||||
'v1.1 优化修复',
|
'v1.1 优化修复',
|
||||||
|
'v1.2 CI/CD 发布',
|
||||||
'v2.0 重构升级',
|
'v2.0 重构升级',
|
||||||
])
|
])
|
||||||
assert.deepEqual(projectRuntime.workflowBatches[1].phases.map((phase) => phase.name), ['优化阶段 1'])
|
assert.deepEqual(projectRuntime.workflowBatches[1].phases.map((phase) => phase.name), ['优化阶段 1'])
|
||||||
|
|||||||
Reference in New Issue
Block a user