Skip to content

Commit 58d45c1

Browse files
committed
fix: align forge with oma core dag
1 parent a742323 commit 58d45c1

4 files changed

Lines changed: 65 additions & 35 deletions

File tree

apps/server/src/runs/session.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ export class RunSession {
221221
this.tasks = [...this.tasks, record]
222222
return
223223
}
224-
this.tasks = this.tasks.map((task, i) => (i === index ? { ...task, ...record } : task))
224+
const prior = this.tasks[index]!
225+
this.tasks = this.tasks.map((task, i) =>
226+
i === index
227+
? {
228+
...task,
229+
...record,
230+
dependsOn:
231+
(record.dependsOn?.length ?? 0) > 0
232+
? record.dependsOn
233+
: (prior.dependsOn ?? []),
234+
}
235+
: task,
236+
)
225237
}
226238
}

apps/server/tests/runs-state.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,46 @@ describe('RunSession', () => {
123123
expect(run.toSnapshot().tasks[0]?.status).toBe('completed')
124124
})
125125

126+
it('preserves dependsOn from the plan when task_start omits it', () => {
127+
const run = new RunSession('run-1', 'Goal')
128+
run.setPlan([
129+
{
130+
id: 't1',
131+
title: 'Research',
132+
status: 'pending',
133+
description: 'Research',
134+
dependsOn: [],
135+
createdAt: new Date(),
136+
updatedAt: new Date(),
137+
},
138+
{
139+
id: 't2',
140+
title: 'Summarize',
141+
status: 'pending',
142+
description: 'Summarize',
143+
dependsOn: ['t1'],
144+
createdAt: new Date(),
145+
updatedAt: new Date(),
146+
},
147+
])
148+
149+
run.applyProgress({
150+
type: 'task_start',
151+
task: 't2',
152+
agent: 'summary-writer',
153+
data: {
154+
id: 't2',
155+
title: 'Summarize',
156+
status: 'in_progress',
157+
description: 'Summarize',
158+
createdAt: new Date(),
159+
updatedAt: new Date(),
160+
},
161+
})
162+
163+
expect(run.toSnapshot().tasks[1]?.dependsOn).toEqual(['t1'])
164+
})
165+
126166
it('preserves dependsOn from the plan when the final result omits it', () => {
127167
const run = new RunSession('run-1', 'Goal')
128168
run.setPlan([

apps/web/src/lib/layout-tasks.ts

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,16 @@ export interface LayoutTasksResult {
1717
readonly nodeH: number
1818
}
1919

20-
/**
21-
* OMA often omits dependsOn on final task records. When no task declares deps,
22-
* infer a simple chain in plan order so the DAG still renders edges.
23-
*/
24-
export function resolveTaskDependencies<T extends LayoutTaskInput>(
25-
taskList: readonly T[],
26-
): readonly (T & { readonly dependsOn: readonly string[] })[] {
27-
const hasExplicit = taskList.some((task) => (task.dependsOn?.length ?? 0) > 0)
28-
if (hasExplicit || taskList.length < 2) {
29-
return taskList.map((task) => ({
30-
...task,
31-
dependsOn: task.dependsOn ?? [],
32-
}))
33-
}
34-
35-
return taskList.map((task, index) => ({
36-
...task,
37-
dependsOn: index === 0 ? [] : [taskList[index - 1]!.id],
38-
}))
39-
}
40-
41-
/** Topological column layout for the team-run DAG canvas. */
20+
/** Topological column layout for the team-run DAG canvas (matches oma-core layout). */
4221
export function layoutTasks<T extends LayoutTaskInput>(
4322
taskList: readonly T[],
4423
): LayoutTasksResult {
45-
const normalized = resolveTaskDependencies(taskList)
46-
const byId = new Map(normalized.map((task) => [task.id, task]))
47-
const children = new Map<string, string[]>(normalized.map((task) => [task.id, []]))
24+
const byId = new Map(taskList.map((task) => [task.id, task]))
25+
const children = new Map<string, string[]>(taskList.map((task) => [task.id, []]))
4826
const indegree = new Map<string, number>()
4927

50-
for (const task of normalized) {
51-
const deps = task.dependsOn.filter((dep) => byId.has(dep))
28+
for (const task of taskList) {
29+
const deps = (task.dependsOn ?? []).filter((dep) => byId.has(dep))
5230
indegree.set(task.id, deps.length)
5331
for (const depId of deps) {
5432
children.get(depId)!.push(task.id)
@@ -58,7 +36,7 @@ export function layoutTasks<T extends LayoutTaskInput>(
5836
const levels = new Map<string, number>()
5937
const queue: string[] = []
6038
let processed = 0
61-
for (const task of normalized) {
39+
for (const task of taskList) {
6240
if ((indegree.get(task.id) ?? 0) === 0) {
6341
levels.set(task.id, 0)
6442
queue.push(task.id)
@@ -79,16 +57,16 @@ export function layoutTasks<T extends LayoutTaskInput>(
7957
}
8058
}
8159

82-
if (processed !== normalized.length) {
60+
if (processed !== taskList.length) {
8361
throw new Error('Task dependency graph contains a cycle')
8462
}
8563

86-
for (const task of normalized) {
64+
for (const task of taskList) {
8765
if (!levels.has(task.id)) levels.set(task.id, 0)
8866
}
8967

90-
const cols = new Map<number, (typeof normalized)[number][]>()
91-
for (const task of normalized) {
68+
const cols = new Map<number, T[]>()
69+
for (const task of taskList) {
9270
const level = levels.get(task.id) ?? 0
9371
if (!cols.has(level)) cols.set(level, [])
9472
cols.get(level)!.push(task)
@@ -116,8 +94,8 @@ export function layoutTasks<T extends LayoutTaskInput>(
11694
}
11795

11896
const edges: LayoutEdge[] = []
119-
for (const task of normalized) {
120-
for (const depId of task.dependsOn) {
97+
for (const task of taskList) {
98+
for (const depId of task.dependsOn ?? []) {
12199
if (byId.has(depId)) edges.push({ fromId: depId, toId: task.id })
122100
}
123101
}
File renamed without changes.

0 commit comments

Comments
 (0)