com.github.copilot/hooks/scripts/Get-SessionElapsed.ps1
|
<#
.SYNOPSIS Reports the elapsed duration of the current chat as one Post-flight line. .DESCRIPTION Reads the session clock the SessionStart hook wrote and prints a single ready-to-paste line naming the elapsed chat duration, the session start, the moment of measurement, and the turn in progress. Post-flight asks the agent to close with that duration, but a model has no clock: a timestamp it composes is a guess, and after a compaction it no longer knows when the session began. Reading it here makes both numbers measured. This is not a VS Code hook - the agent runs it, so the output is plain text on standard output rather than the hook JSON contract. Read-only by design. The Stop hook owns the turn counter, so this reports the turn in progress as one past the closed count and writes nothing back. .PARAMETER Path Exact session clock file to read. Defaults to the newest clock recorded for the current workspace, which is what survives a compaction that dropped the injected session context. .PARAMETER ClockRoot Directory holding the session clock files. Defaults to the per-user application data location. Tests override it to stay off the real profile. .PARAMETER WorkingDirectory Workspace used to pick between concurrent sessions. Defaults to the current directory. .EXAMPLE & "$HOME/.copilot/hooks/scripts/Get-SessionElapsed.ps1" POST-FLIGHT elapsed: 16m (started 09:15 UTC, measured 09:31 UTC, turn 3) .NOTES Never fails: every path exits 0, so an unreadable clock costs the duration line and nothing else. #> [CmdletBinding()] [OutputType([string])] param( [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$Path, [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$ClockRoot, [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$WorkingDirectory ) function Format-Elapsed { <# Duplicated verbatim in Write-SessionClose.ps1 so both sides render the same duration the same way. Change them together. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [timespan]$Duration ) if ($Duration.TotalSeconds -lt 0) { return 'unavailable' } if ($Duration.TotalMinutes -lt 1) { return 'under a minute' } # Floor explicitly: a cast to int rounds, so 90 minutes would report 2h 30m. if ($Duration.TotalHours -lt 1) { return '{0}m' -f [int][Math]::Floor($Duration.TotalMinutes) } return '{0}h {1:00}m' -f [int][Math]::Floor($Duration.TotalHours), $Duration.Minutes } $measuredUtc = (Get-Date).ToUniversalTime() $unavailable = 'POST-FLIGHT elapsed: unavailable (no session clock on disk).' if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) { $WorkingDirectory = (Get-Location -PSProvider FileSystem).ProviderPath } if ([string]::IsNullOrWhiteSpace($ClockRoot)) { # Per-user by construction, matching Get-SessionClockPath in the hooks. The # temp directory is world-writable on Linux, where a predictable name invites # another local account to pre-create the path. $ClockRoot = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) if ([string]::IsNullOrWhiteSpace($ClockRoot)) { $ClockRoot = [IO.Path]::GetTempPath() } $ClockRoot = [IO.Path]::Combine($ClockRoot, 'CopilotAtelier', 'sessions') } try { if ([string]::IsNullOrWhiteSpace($Path) -or -not [IO.File]::Exists($Path)) { $candidates = @( Get-ChildItem -LiteralPath $ClockRoot -Filter 'session-*.json' -File -ErrorAction Stop | Sort-Object -Property LastWriteTimeUtc -Descending ) # Two windows on different workspaces keep separate clocks, so prefer the # one this workspace started before falling back to the newest overall. $matching = @( $candidates | Where-Object { try { $recorded = [IO.File]::ReadAllText($_.FullName) | ConvertFrom-Json -ErrorAction Stop [string]$recorded.workspace -eq $WorkingDirectory } catch { $false } } ) <# VS Code always supplies session_id, so a live session is always keyed by it. The cwd hash is the fallback for a payload that omits one, which in practice means a test or a non-VS-Code caller - and such a file is newer often enough to shadow the real session. #> $Path = @( $matching | Where-Object { $_.Name -notlike 'session-cwd-*' } $matching $candidates ) | Select-Object -First 1 -ExpandProperty FullName } $clock = [IO.File]::ReadAllText($Path) | ConvertFrom-Json -ErrorAction Stop $startedUtc = ([datetimeoffset]$clock.startedUtc).UtcDateTime # The Stop hook increments only once the turn is closed, so the turn being # reported on is one past the recorded count. $turn = [int]$clock.turns + 1 Write-Output -InputObject ('POST-FLIGHT elapsed: {0} (started {1} UTC, measured {2} UTC, turn {3})' -f (Format-Elapsed -Duration ($measuredUtc - $startedUtc)), $startedUtc.ToString('HH:mm'), $measuredUtc.ToString('HH:mm'), $turn) } catch { Write-Output -InputObject $unavailable } exit 0 |