@@ -147,31 +147,96 @@ export function isTaskClaimableBy(
147147
148148export function areTaskDependenciesSatisfied (
149149 task : Pick < AgentTask , "depends_on" > ,
150- completedTaskIds : ReadonlySet < string > ,
150+ taskStates : ReadonlyMap < string , Pick < AgentTask , "status" | "replacement_task_id" > > ,
151151) : boolean {
152- return task . depends_on . every ( ( dep ) => completedTaskIds . has ( dep ) ) ;
152+ return task . depends_on . every ( ( dep ) => isDependencyResolved ( dep , taskStates ) ) ;
153153}
154154
155- async function readCompletedTaskIds ( contextDir : string ) : Promise < Set < string > > {
156- const completedDir = tasksStatusDir ( contextDir , "completed" ) ;
157- let completedEntries : string [ ] ;
158- try {
159- completedEntries = await readdir ( completedDir ) ;
160- } catch {
161- completedEntries = [ ] ;
162- }
155+ function isDependencyResolved (
156+ taskId : string ,
157+ taskStates : ReadonlyMap < string , Pick < AgentTask , "status" | "replacement_task_id" > > ,
158+ visiting : Set < string > = new Set ( ) ,
159+ ) : boolean {
160+ const state = taskStates . get ( taskId ) ;
161+ if ( ! state ) return false ;
162+ if ( state . status === "completed" ) return true ;
163+ if ( state . status !== "superseded" ) return false ;
164+ if ( ! state . replacement_task_id ) return true ;
165+ if ( visiting . has ( taskId ) ) return false ;
166+ visiting . add ( taskId ) ;
167+ const resolved = isDependencyResolved ( state . replacement_task_id , taskStates , visiting ) ;
168+ visiting . delete ( taskId ) ;
169+ return resolved ;
170+ }
163171
164- return new Set (
165- completedEntries
166- . filter ( ( f ) => f . endsWith ( ".json" ) )
167- . map ( ( f ) => f . replace ( ".json" , "" ) ) ,
172+ async function readTaskDependencyStates (
173+ contextDir : string ,
174+ ) : Promise < Map < string , Pick < AgentTask , "status" | "replacement_task_id" > > > {
175+ const tasks = await listTasks ( contextDir ) ;
176+ return new Map (
177+ tasks . map ( ( task ) => [
178+ task . task_id ,
179+ {
180+ status : task . status ,
181+ replacement_task_id : task . replacement_task_id ,
182+ } ,
183+ ] ) ,
168184 ) ;
169185}
170186
187+ async function unblockSatisfiedBlockedTasks ( contextDir : string ) : Promise < number > {
188+ const blocked = await listTasks ( contextDir , "blocked" ) ;
189+ if ( blocked . length === 0 ) return 0 ;
190+
191+ const taskStates = await readTaskDependencyStates ( contextDir ) ;
192+ const blockedDir = tasksStatusDir ( contextDir , "blocked" ) ;
193+ const pendingDir = tasksStatusDir ( contextDir , "pending" ) ;
194+ const tmpDir = tasksTmp ( contextDir ) ;
195+ await mkdir ( pendingDir , { recursive : true } ) ;
196+
197+ let unblocked = 0 ;
198+ for ( const task of blocked ) {
199+ if ( ! areTaskDependenciesSatisfied ( task , taskStates ) ) continue ;
200+
201+ const filename = `${ task . task_id } .json` ;
202+ const blockedPath = resolve ( blockedDir , filename ) ;
203+ const pendingPath = resolve ( pendingDir , filename ) ;
204+ const now = Date . now ( ) ;
205+ task . status = "pending" ;
206+ task . updated_at = now ;
207+ task . claimed_by = null ;
208+ task . claimed_at = null ;
209+
210+ try {
211+ await atomicJsonWrite ( tmpDir , blockedPath , task ) ;
212+ await rename ( blockedPath , pendingPath ) ;
213+ } catch {
214+ continue ;
215+ }
216+
217+ taskStates . set ( task . task_id , {
218+ status : "pending" ,
219+ replacement_task_id : task . replacement_task_id ,
220+ } ) ;
221+
222+ await appendTaskEvent ( contextDir , {
223+ ts : now ,
224+ type : "task_requeued" ,
225+ task_id : task . task_id ,
226+ title : task . title ,
227+ } ) ;
228+ unblocked ++ ;
229+ }
230+
231+ return unblocked ;
232+ }
233+
171234export async function hasActionableTaskForMember (
172235 contextDir : string ,
173236 memberId : string ,
174237) : Promise < boolean > {
238+ await unblockSatisfiedBlockedTasks ( contextDir ) ;
239+
175240 const inProgress = await listTasks ( contextDir , "in_progress" ) ;
176241 if ( inProgress . some ( ( task ) => task . claimed_by === memberId ) ) {
177242 return true ;
@@ -183,9 +248,9 @@ export async function hasActionableTaskForMember(
183248 return false ;
184249 }
185250
186- const completedTaskIds = await readCompletedTaskIds ( contextDir ) ;
251+ const taskStates = await readTaskDependencyStates ( contextDir ) ;
187252 return claimablePending . some ( ( task ) =>
188- areTaskDependenciesSatisfied ( task , completedTaskIds ) ,
253+ areTaskDependenciesSatisfied ( task , taskStates ) ,
189254 ) ;
190255}
191256
@@ -249,8 +314,8 @@ export async function claimTask(
249314
250315 // Check dependencies
251316 if ( task . depends_on . length > 0 ) {
252- const completedIds = await readCompletedTaskIds ( contextDir ) ;
253- const unmet = task . depends_on . filter ( ( dep ) => ! completedIds . has ( dep ) ) ;
317+ const taskStates = await readTaskDependencyStates ( contextDir ) ;
318+ const unmet = task . depends_on . filter ( ( dep ) => ! isDependencyResolved ( dep , taskStates ) ) ;
254319 if ( unmet . length > 0 ) {
255320 // Move to blocked/
256321 const blockedPath = resolve ( blockedDir , filename ) ;
@@ -394,6 +459,8 @@ export async function completeTask(
394459 by : memberId ,
395460 } ) ;
396461
462+ await unblockSatisfiedBlockedTasks ( contextDir ) ;
463+
397464 return { ok : true } ;
398465}
399466
@@ -467,5 +534,7 @@ export async function supersedeTask(
467534 replacement_task_id : replacementTaskId ,
468535 } ) ;
469536
537+ await unblockSatisfiedBlockedTasks ( contextDir ) ;
538+
470539 return { ok : true } ;
471540}
0 commit comments