Private/Context.ps1

# ══════════════════════════════════════════════════════════════════════════════
# $global:context — conversation log captured via Out-Default proxy
# ══════════════════════════════════════════════════════════════════════════════
#
# The user's interactive shell pipes the result of every prompt-level command
# through Out-Default. By shadowing Out-Default with a proxy function we can
# append each (command, output) pair to $global:context while still rendering
# normally. Agents, swarms, and Build-SystemPrompt read from $global:context
# so the LLM sees the same conversation the user is living in.
#
# Entries are [PSCustomObject]:
# HistoryId [int] — PS command history id (Human only), or -1
# Timestamp [datetime] — UTC time the entry was appended
# Source [string] — 'Human' | 'Agent' | 'Swarm'
# Command [string] — for Human: Get-History command line;
# for Agent: the expression the agent ran;
# for Swarm: the subtask description
# Output [List[object]] — the LIVE typed .NET objects

if (-not (Get-Variable -Name 'context' -Scope Global -ErrorAction SilentlyContinue)) {
    $global:context = [System.Collections.Generic.List[PSCustomObject]]::new()
}

# Set by the PSReadLine AddToHistoryHandler (see Install-ContextCapture) the
# instant the user presses Enter, BEFORE the pipeline executes. Out-Default
# reads this during its end{} to tag the captured output with the right
# command line — Get-History doesn't yet contain the current command at that
# point, and $MyInvocation.HistoryId inside the proxy is off by one.
$script:LastHumanCommand = ''
$script:LastHumanHid     = 0
$script:_pwrcortex_prevHistHandler = $null

function script:Add-ContextEntry {
    [CmdletBinding()]
    param(
        [System.Collections.Generic.IList[object]]$Items,

        [ValidateSet('Human','Agent','Swarm')]
        [string]$Source = 'Human',

        [string]$Command = '',

        # Supplied by the Out-Default proxy via $MyInvocation.HistoryId so
        # we attribute output to the CURRENT interactive command (not the
        # previous one, which is what Get-History -Count 1 would return
        # mid-pipeline).
        [int]$HistoryId = -1
    )

    if (-not $Items -or $Items.Count -eq 0) { return }
    if (-not $global:context) {
        $global:context = [System.Collections.Generic.List[PSCustomObject]]::new()
    }

    $cmdId   = $HistoryId
    $cmdLine = $Command

    if ($Source -eq 'Human') {
        # A single interactive command may call Out-Default more than once
        # (mixed format blocks, multiple return values). Merge into the tail
        # entry when it's the same Human command so one command == one entry.
        # Never merge across Source boundaries.
        $tail = if ($global:context.Count -gt 0) {
            $global:context[$global:context.Count - 1]
        } else { $null }

        if ($tail -and $tail.Source -eq 'Human' -and $cmdId -gt 0 -and
            $tail.HistoryId -eq $cmdId) {
            foreach ($i in $Items) { $tail.Output.Add($i) }
            # Backfill Command if the earlier entry missed it.
            if (-not $tail.Command -and $cmdLine) { $tail.Command = $cmdLine }
            return
        }
    }

    $entry = [PSCustomObject]@{
        HistoryId = $cmdId
        Timestamp = [datetime]::UtcNow
        Source    = $Source
        Command   = $cmdLine
        Output    = [System.Collections.Generic.List[object]]::new($Items)
    }
    $global:context.Add($entry)
}

function script:Install-ContextCapture {
    [CmdletBinding()]
    param()

    # Resolve the real cmdlet up-front so the proxy can call it without
    # re-entering command resolution (and without recursing into itself).
    $realOutDefault = $ExecutionContext.InvokeCommand.GetCommand(
        'Microsoft.PowerShell.Core\Out-Default',
        [System.Management.Automation.CommandTypes]::Cmdlet)
    if (-not $realOutDefault) {
        Write-Warning "PwrCortex: Out-Default cmdlet not found; context capture disabled."
        return
    }

    # GetNewClosure() binds $realOutDefault into the proxy so later
    # redefinitions of the cmdlet don't affect us.
    $proxyBody = {
        [CmdletBinding()]
        param(
            [switch]$Transcript,
            [Parameter(ValueFromPipeline=$true)]
            [psobject]$InputObject
        )
        begin {
            try {
                $script:__pwrcortex_items = [System.Collections.Generic.List[object]]::new()
                $outBuffer = $null
                if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                    $PSBoundParameters['OutBuffer'] = 1
                }
                $scriptCmd = { & $realOutDefault @PSBoundParameters }
                $script:__pwrcortex_sp = $scriptCmd.GetSteppablePipeline($MyInvocation.CommandOrigin)
                $script:__pwrcortex_sp.Begin($PSCmdlet)
            } catch { throw }
        }
        process {
            try {
                if ($null -ne $_) { $script:__pwrcortex_items.Add($_) }
                $script:__pwrcortex_sp.Process($_)
            } catch { throw }
        }
        end {
            try { $script:__pwrcortex_sp.End() }
            finally {
                try {
                    # Pull the captured command from the PSReadLine
                    # AddToHistoryHandler, which ran before the pipeline
                    # started and has the exact line the user pressed Enter
                    # on. Fall back to Get-History if PSReadLine wasn't loaded
                    # (e.g. piped / non-interactive pwsh), accepting that it
                    # is one command stale in that case.
                    & (Get-Module PwrCortex) {
                        param($i)
                        $cmd = $script:LastHumanCommand
                        $hid = $script:LastHumanHid
                        if (-not $cmd) {
                            $h = Get-History -Count 1 -ErrorAction SilentlyContinue
                            if ($h) {
                                $cmd = [string]$h.CommandLine
                                $hid = [int]$h.Id
                            }
                        }
                        script:Add-ContextEntry -Items $i -HistoryId $hid -Command $cmd
                    } $script:__pwrcortex_items
                } catch {
                    Write-Debug "PwrCortex: failed to append context entry: $_"
                }
            }
        }
    }.GetNewClosure()

    Set-Item -Path function:global:Out-Default -Value $proxyBody -Force

    # Install a PSReadLine AddToHistoryHandler that captures the user's
    # command the instant Enter is pressed — before the pipeline runs.
    # Out-Default's end{} reads $script:LastHumanCommand to tag the buffered
    # output with the right line. Without this hook, neither Get-History nor
    # $MyInvocation.HistoryId give the correct value inside Out-Default: the
    # entry doesn't land in history until after the pipeline (including
    # Out-Default) completes.
    if (Get-Module -Name PSReadLine -ErrorAction SilentlyContinue) {
        try {
            $existing = (Get-PSReadLineOption).AddToHistoryHandler
            $script:_pwrcortex_prevHistHandler = $existing
            Set-PSReadLineOption -AddToHistoryHandler {
                param([string]$line)
                & (Get-Module PwrCortex) {
                    param($l)
                    $script:LastHumanCommand = $l
                    $script:LastHumanHid++
                } $line
                if ($script:_pwrcortex_prevHistHandler) {
                    return & $script:_pwrcortex_prevHistHandler $line
                }
                return 'MemoryAndFile'
            }.GetNewClosure()
            Write-Verbose "PwrCortex: PSReadLine AddToHistoryHandler installed for command-line capture."
        } catch {
            Write-Warning "PwrCortex: failed to install PSReadLine handler; context commands may be stale. $_"
        }
    } else {
        Write-Verbose "PwrCortex: PSReadLine not loaded; context will fall back to Get-History (one-command stale)."
    }

    Write-Verbose "PwrCortex: Out-Default proxy installed; `$global:context capture active."
}

function script:Uninstall-ContextCapture {
    [CmdletBinding()]
    param()
    # Remove-Item function:global:Out-Default reports success but silently
    # leaves the function in place (PS 7.x quirk with scoped function: paths).
    # The pipeline form via Get-ChildItem works reliably.
    $removed = 0
    Get-ChildItem function: |
        Where-Object { $_.Name -eq 'Out-Default' } |
        ForEach-Object {
            Remove-Item -LiteralPath "function:$($_.Name)" -Force -ErrorAction SilentlyContinue
            $removed++
        }
    if ($removed -gt 0) {
        Write-Verbose "PwrCortex: Out-Default proxy removed."
    }

    if (Get-Module -Name PSReadLine -ErrorAction SilentlyContinue) {
        try {
            if ($script:_pwrcortex_prevHistHandler) {
                Set-PSReadLineOption -AddToHistoryHandler $script:_pwrcortex_prevHistHandler
            } else {
                Set-PSReadLineOption -AddToHistoryHandler $null
            }
            $script:_pwrcortex_prevHistHandler = $null
            Write-Verbose "PwrCortex: PSReadLine AddToHistoryHandler restored."
        } catch {
            Write-Warning "PwrCortex: failed to restore PSReadLine handler. $_"
        }
    }
}