Skip to content

Archiving a thread does not archive spawned child threads #17867

Closed

Description

@Vadevious

What version of the Codex App are you using (From “About Codex” dialog)?

26.409.20454

What subscription do you have?

Pro 20x(10x)

What platform is your computer?

Microsoft Windows NT 10.0.19045.0 x64

What issue are you seeing?

Archiving a main thread does not archive the spawned child/sub-agent threads associated with it.

I archived parent thread 019d6f6f-c048-7283-8871-ef0a87e778ea. The parent thread is marked archived, but its spawned descendants remain live. In local Codex state, that parent has 177 child threads in thread_spawn_edges, and all 177 children still have threads.archived = 0.

Many of those child threads inherit the same original generated title, which leaves a large amount of clutter in the thread list and makes the archive feel incomplete.

What steps can reproduce the bug?

  1. Start a thread that uses spawned child/sub-agent threads.
  2. Let the child threads complete so they are closed but still present in local state.
  3. Archive the parent thread from the Codex app.
  4. Inspect the archive/live state afterward.

Observed result:

  • the parent thread is archived
  • the child threads remain unarchived/live
  • descendant rollout/session state is still left behind

What is the expected behavior?

Archiving a parent thread should either:

  1. recursively archive its spawned descendants by default, or
  2. prompt the user to also archive the spawned child threads.

At minimum, the UI should make it clear that descendant threads remain live after archiving the parent.

Additional information

Example parent thread:

  • 019d6f6f-c048-7283-8871-ef0a87e778ea

Local evidence:

  • 177 descendants recorded in thread_spawn_edges
  • all 177 descendants still had threads.archived = 0
  • many descendant threads inherited the same original generated title as the parent

I can provide descendant thread IDs if useful.

Activity

  1. added
    appIssues related to the Codex desktop app
    on Apr 15, 2026
  2. added
    bugSomething isn't working
    subagentIssues involving subagents or multi-agent features
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Apr 15, 2026
  3. github-actions commented on Apr 15, 2026

    @github-actions
    Contributor

    Potential duplicates detected. Please review them and close your issue if it is a duplicate.

    Powered by Codex Action

  4. Vadevious commented on Apr 15, 2026

    @Vadevious
    Author

    Here's a Codex-made script that I use as a workaround currently, to help clean up the bloat.

    It does three things:

    • finds descendant threads via thread_spawn_edges
    • moves their rollout files from sessions to archived_sessions
    • updates threads.archived, threads.archived_at, threads.updated_at, and threads.rollout_path

    It supports:

    • -DryRun while Codex is open
    • real mutation only when Codex is closed
    param(
        [string]$StateDb = "$HOME\\.codex\\state_5.sqlite",
        [string]$ArchivedRoot = "$HOME\\.codex\\archived_sessions",
        [string]$ThreadId,
        [switch]$DryRun
    )
    
    $ErrorActionPreference = 'Stop'
    
    function Get-CodexProcesses {
        Get-Process -ErrorAction SilentlyContinue |
            Where-Object { $_.ProcessName -in @('Codex', 'codex', 'CodexAccountDashboard') }
    }
    
    function Invoke-SqlCsv {
        param([string]$Sql)
        sqlite3 -header -csv $StateDb $Sql | ConvertFrom-Csv
    }
    
    function Normalize-Path {
        param([string]$Path)
        if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
        try {
            return [System.IO.Path]::GetFullPath($Path)
        } catch {
            return $Path
        }
    }
    
    $codexProcesses = Get-CodexProcesses
    if ($codexProcesses -and -not $DryRun) {
        $processList = ($codexProcesses | Sort-Object ProcessName, Id | ForEach-Object { '{0}:{1}' -f $_.ProcessName, $_.Id }) -join ', '
        throw "Refusing to archive thread trees while Codex processes are running: $processList"
    }
    
    if (-not (Test-Path -LiteralPath $StateDb)) {
        throw "Missing Codex state DB: $StateDb"
    }
    
    if (-not (Test-Path -LiteralPath $ArchivedRoot)) {
        New-Item -ItemType Directory -Force -Path $ArchivedRoot | Out-Null
    }
    
    $threads = Invoke-SqlCsv @"
    SELECT
      id,
      archived,
      archived_at,
      updated_at,
      rollout_path,
      REPLACE(REPLACE(title, char(13), ' '), char(10), ' ') AS title
    FROM threads;
    "@
    
    $edges = Invoke-SqlCsv @"
    SELECT parent_thread_id, child_thread_id, status
    FROM thread_spawn_edges;
    "@
    
    $threadMap = @{}
    foreach ($thread in $threads) {
        $thread.rollout_path = Normalize-Path $thread.rollout_path
        $threadMap[$thread.id] = $thread
    }
    
    $childrenByParent = @{}
    foreach ($edge in $edges) {
        if (-not $childrenByParent.ContainsKey($edge.parent_thread_id)) {
            $childrenByParent[$edge.parent_thread_id] = [System.Collections.Generic.List[string]]::new()
        }
        $childrenByParent[$edge.parent_thread_id].Add($edge.child_thread_id)
    }
    
    if ($ThreadId) {
        if (-not $threadMap.ContainsKey($ThreadId)) {
            throw "Thread not found in DB: $ThreadId"
        }
        $rootIds = @($ThreadId)
    } else {
        $rootIds = @()
        foreach ($parentId in $childrenByParent.Keys) {
            if (-not $threadMap.ContainsKey($parentId)) { continue }
            $parent = $threadMap[$parentId]
            if ([int]$parent.archived -ne 1) { continue }
    
            $hasUnarchivedChild = $false
            foreach ($childId in $childrenByParent[$parentId]) {
                if ($threadMap.ContainsKey($childId) -and ([int]$threadMap[$childId].archived -ne 1)) {
                    $hasUnarchivedChild = $true
                    break
                }
            }
    
            if ($hasUnarchivedChild) {
                $rootIds += $parentId
            }
        }
        $rootIds = $rootIds | Sort-Object -Unique
    }
    
    if (-not $rootIds) {
        Write-Output 'No archived parent threads with unarchived descendants were found.'
        exit 0
    }
    
    $visited = [System.Collections.Generic.HashSet[string]]::new()
    $targets = [System.Collections.Generic.List[object]]::new()
    
    foreach ($rootId in $rootIds) {
        if (-not $childrenByParent.ContainsKey($rootId)) { continue }
    
        $queue = [System.Collections.Generic.Queue[string]]::new()
        foreach ($childId in $childrenByParent[$rootId]) {
            $queue.Enqueue($childId)
        }
    
        while ($queue.Count -gt 0) {
            $childId = $queue.Dequeue()
            if (-not $visited.Add($childId)) { continue }
            if (-not $threadMap.ContainsKey($childId)) { continue }
    
            $thread = $threadMap[$childId]
            $targets.Add([pscustomobject]@{
                root_id = $rootId
                thread_id = $childId
                title = $thread.title
                archived = [int]$thread.archived
                rollout_path = $thread.rollout_path
            })
    
            if ($childrenByParent.ContainsKey($childId)) {
                foreach ($nextId in $childrenByParent[$childId]) {
                    $queue.Enqueue($nextId)
                }
            }
        }
    }
    
    if (-not $targets) {
        Write-Output 'No descendant threads were found for the selected roots.'
        exit 0
    }
    
    $now = [DateTimeOffset]::Now.ToUnixTimeSeconds()
    $results = [System.Collections.Generic.List[object]]::new()
    
    foreach ($target in $targets) {
        if ($target.archived -eq 1) {
            $results.Add([pscustomobject]@{
                root_id = $target.root_id
                thread_id = $target.thread_id
                action = 'already_archived'
                from_path = $target.rollout_path
                to_path = $target.rollout_path
                note = 'DB already marked archived'
            })
            continue
        }
    
        $sourcePath = $target.rollout_path
        $destPath = if ($sourcePath) { Join-Path $ArchivedRoot ([System.IO.Path]::GetFileName($sourcePath)) } else { $null }
        $sourceExists = $sourcePath -and (Test-Path -LiteralPath $sourcePath)
        $destExists = $destPath -and (Test-Path -LiteralPath $destPath)
    
        if (-not $sourceExists -and -not $destExists) {
            $results.Add([pscustomobject]@{
                root_id = $target.root_id
                thread_id = $target.thread_id
                action = 'skipped_missing_rollout'
                from_path = $sourcePath
                to_path = $destPath
                note = 'Neither source nor archive rollout file exists'
            })
            continue
        }
    
        if (-not $DryRun) {
            if ($sourceExists -and -not $destExists) {
                Move-Item -LiteralPath $sourcePath -Destination $destPath -Force
            }
    
            $dbPath = if ($destExists -or (Test-Path -LiteralPath $destPath)) { $destPath } else { $sourcePath }
            $safePath = if ($dbPath) { $dbPath.Replace("'", "''") } else { '' }
    
            sqlite3 $StateDb @"
    UPDATE threads
    SET archived = 1,
        archived_at = $now,
        updated_at = $now,
        rollout_path = '$safePath'
    WHERE id = '$($target.thread_id)';
    "@ | Out-Null
        }
    
        $results.Add([pscustomobject]@{
            root_id = $target.root_id
            thread_id = $target.thread_id
            action = if ($DryRun) { 'would_archive' } else { 'archived' }
            from_path = $sourcePath
            to_path = $destPath
            note = if ($sourceExists -and -not $destExists) { 'Moved rollout and updated DB' } elseif ($destExists) { 'Archive rollout already existed; updated DB only' } else { 'Updated DB only' }
        })
    }
    
    $summary = [pscustomobject]@{
        root_count = ($rootIds | Measure-Object).Count
        descendant_count = $targets.Count
        archived_count = ($results | Where-Object action -eq 'archived').Count
        would_archive_count = ($results | Where-Object action -eq 'would_archive').Count
        already_archived_count = ($results | Where-Object action -eq 'already_archived').Count
        skipped_missing_rollout_count = ($results | Where-Object action -eq 'skipped_missing_rollout').Count
    }
    
    $summary
    $results | Sort-Object root_id, thread_id
    
    
  5. removed
    appIssues related to the Codex desktop app
    on Apr 15, 2026
  6. added a commit that references this issue on Apr 20, 2026
  7. added a commit that references this issue on May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsessionIssues involving session (thread) management, resuming, forking, naming, archivingsubagentIssues involving subagents or multi-agent features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions