← Files Chronos for CodexARCHIVED FILE
skills/chronos/scripts/session-registry.ps1
95.3 KB · Oct 2, 2026 · 00:30 UTC
param(
[ValidateSet('hook', 'status', 'initialize', 'confirm-active', 'reconcile-host', 'discover', 'cycle', 'release')]
[string]$Action = 'status',
[string]$StatePath,
[string]$CodexHome,
[string]$HostInventoryPath,
[string]$SessionId = $env:CODEX_THREAD_ID,
[string]$SubjectId,
[string]$ObservedAtUtc,
[ValidateRange(0, [long]::MaxValue)]
[long]$SinceRevision = 0,
[switch]$ConfirmRecurrenceStopped,
[switch]$Diagnostic,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$script:StateByteLimit = 262144
$script:HookInputByteLimit = 65536
$script:SessionLimit = 256
$script:ResultLimit = 64
$script:CheckBatchLimit = 8
$script:EndedRetentionHours = 24
$script:ActiveRetentionDays = 30
$script:JsonNodeLimit = 8192
$script:PendingEventLimit = 256
$script:PendingEventReceiptLimit = $script:PendingEventLimit * 3
$script:PendingEventByteLimit = 4096
$script:PendingEventWriteAttemptLimit = 2
$script:PendingEventRetryDelayMilliseconds = 10
$script:SynchronousHookMutexWaitMilliseconds = 250
$script:AsynchronousHookMutexWaitMilliseconds = 100
$script:GovernorActiveCadenceMinutes = 60
$script:GovernorIdleCadenceMinutes = 360
$script:GovernorMaximumCycles = 336
$script:GovernorMaximumAgeDays = 14
$script:GovernorEquivalencePrefix = 'chronos-supervision-v1'
$script:HostEquivalenceKey = $script:GovernorEquivalencePrefix
$script:HostReconcileAttemptLimit = 3
$script:HostRecheckThroughCycle = 2
$script:Entropy = [Text.Encoding]::UTF8.GetBytes('Chronos.Supervision.Registry.v1')
$script:InvocationObservedAtUtc = [DateTimeOffset]::UtcNow
$script:HostInventoryByteLimit = 131072
$script:StateStoreMode = 'unknown'
$script:StateStoreMigration = 'not_applicable'
$script:StateStoreWriteReady = $false
$script:StateStoreProtection = 'dpapi_ids'
$script:LegacyDefaultStatePath = $null
$script:LegacyInstallationScopePath = $null
$script:PriorDefaultStatePath = $null
$script:PriorInstallationScopePath = $null
$script:PriorV2DefaultStatePath = $null
$script:PriorV2InstallationScopePath = $null
$script:AllowUnscopedLegacyMigration = $false
$script:PriorStateDisposition = 'not_checked'
$script:PriorStateWriteAttempted = $false
$script:PriorMigrationBlocked = $false
$script:DefaultStateCandidates = @()
$script:DefaultInstallationScopeId = $null
$script:RecoveredV3InstallationScopeId = $null
$script:InstallationScopeSource = 'unresolved'
$script:StateStoreSlot = -1
$script:StateStoreRecovery = 'not_applicable'
$script:StateStoreIdentity = 'unresolved'
$script:CodexHomeSource = 'unresolved'
$script:CodexHomeIdentity = 'unresolved'
$script:RegistryMutexIdentity = 'unresolved'
$script:HookInboxDirectory = $null
function Get-Value {
param($Object, [string]$Name, $Default = $null)
if ($null -eq $Object) { return $Default }
if ($Object -is [Collections.IDictionary]) {
if ($Object.Contains($Name) -and $null -ne $Object[$Name]) { return $Object[$Name] }
return $Default
}
$property = $Object.PSObject.Properties[$Name]
if ($null -eq $property -or $null -eq $property.Value) { return $Default }
return $property.Value
}
function ConvertTo-Hashtable {
param($Value)
if ($null -eq $Value) { return $null }
if ($Value -is [Collections.IDictionary]) {
$copy = @{}
foreach ($key in $Value.Keys) { $copy[[string]$key] = ConvertTo-Hashtable $Value[$key] }
return $copy
}
if ($Value.GetType().FullName -eq 'System.Management.Automation.PSCustomObject') {
$copy = @{}
foreach ($property in $Value.PSObject.Properties) { $copy[$property.Name] = ConvertTo-Hashtable $property.Value }
return $copy
}
if ($Value -is [Collections.IEnumerable] -and -not ($Value -is [string])) {
return ,@($Value | ForEach-Object { ConvertTo-Hashtable $_ })
}
return $Value
}
function Move-JsonWhitespace {
param([string]$Text, [ref]$Index)
while ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -in @(' ', "`t", "`r", "`n")) { $Index.Value++ }
}
function Read-StrictJsonString {
param([string]$Text, [ref]$Index, [string]$ErrorCode)
if ($Index.Value -ge $Text.Length -or $Text[$Index.Value] -ne '"') { throw $ErrorCode }
$start = $Index.Value
$Index.Value++
while ($Index.Value -lt $Text.Length) {
$character = $Text[$Index.Value]
if ([int][char]$character -lt 0x20) { throw $ErrorCode }
if ($character -eq '"') {
$Index.Value++
try {
$decoded = $Text.Substring($start, $Index.Value - $start) | ConvertFrom-Json -ErrorAction Stop
if (-not ($decoded -is [string])) { throw $ErrorCode }
return $decoded.Normalize([Text.NormalizationForm]::FormC)
} catch { throw $ErrorCode }
}
if ($character -eq '\') {
$Index.Value++
if ($Index.Value -ge $Text.Length) { throw $ErrorCode }
$escape = $Text[$Index.Value]
if ($escape -eq 'u') {
if ($Index.Value + 4 -ge $Text.Length -or $Text.Substring($Index.Value + 1, 4) -notmatch '^[0-9A-Fa-f]{4}$') { throw $ErrorCode }
$Index.Value += 5
continue
}
if ($escape -notin @('"', '\', '/', 'b', 'f', 'n', 'r', 't')) { throw $ErrorCode }
}
$Index.Value++
}
throw $ErrorCode
}
function Assert-StrictJsonValue {
param([string]$Text, [ref]$Index, [ref]$NodeCount, [int]$Depth, [string]$ErrorCode)
if ($Depth -gt 16) { throw $ErrorCode }
Move-JsonWhitespace $Text $Index
if ($Index.Value -ge $Text.Length) { throw $ErrorCode }
$NodeCount.Value++
if ($NodeCount.Value -gt $script:JsonNodeLimit) { throw $ErrorCode }
$character = $Text[$Index.Value]
if ($character -eq '"') { [void](Read-StrictJsonString $Text $Index $ErrorCode); return }
if ($character -eq '{') {
$Index.Value++
Move-JsonWhitespace $Text $Index
$keys = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
if ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -eq '}') { $Index.Value++; return }
while ($true) {
Move-JsonWhitespace $Text $Index
$key = Read-StrictJsonString $Text $Index $ErrorCode
if (-not $keys.Add($key)) { throw $ErrorCode }
Move-JsonWhitespace $Text $Index
if ($Index.Value -ge $Text.Length -or $Text[$Index.Value] -ne ':') { throw $ErrorCode }
$Index.Value++
Assert-StrictJsonValue $Text $Index $NodeCount ($Depth + 1) $ErrorCode
Move-JsonWhitespace $Text $Index
if ($Index.Value -ge $Text.Length) { throw $ErrorCode }
if ($Text[$Index.Value] -eq '}') { $Index.Value++; return }
if ($Text[$Index.Value] -ne ',') { throw $ErrorCode }
$Index.Value++
}
}
if ($character -eq '[') {
$Index.Value++
Move-JsonWhitespace $Text $Index
if ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -eq ']') { $Index.Value++; return }
while ($true) {
Assert-StrictJsonValue $Text $Index $NodeCount ($Depth + 1) $ErrorCode
Move-JsonWhitespace $Text $Index
if ($Index.Value -ge $Text.Length) { throw $ErrorCode }
if ($Text[$Index.Value] -eq ']') { $Index.Value++; return }
if ($Text[$Index.Value] -ne ',') { throw $ErrorCode }
$Index.Value++
}
}
$start = $Index.Value
while ($Index.Value -lt $Text.Length -and $Text[$Index.Value] -notin @(',', ']', '}', ' ', "`t", "`r", "`n")) { $Index.Value++ }
$token = $Text.Substring($start, $Index.Value - $start)
if ($token -notmatch '^(?:true|false|null|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)$') { throw $ErrorCode }
}
function Assert-StrictJson {
param([string]$Text, [string]$ErrorCode)
if ([string]::IsNullOrWhiteSpace($Text)) { throw $ErrorCode }
$index = 0
$nodeCount = 0
Assert-StrictJsonValue $Text ([ref]$index) ([ref]$nodeCount) 0 $ErrorCode
Move-JsonWhitespace $Text ([ref]$index)
if ($index -ne $Text.Length) { throw $ErrorCode }
}
function Test-IsInteger {
param($Value)
return ($Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or
$Value -is [uint16] -or $Value -is [int32] -or $Value -is [uint32] -or
$Value -is [int64] -or $Value -is [uint64])
}
function Get-TextHash {
param([string]$Value)
$sha = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Value)))).Replace('-', '').ToLowerInvariant()
} finally {
$sha.Dispose()
}
}
function Get-BytesHash {
param([byte[]]$Value)
$sha = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($sha.ComputeHash($Value))).Replace('-', '').ToLowerInvariant()
} finally {
$sha.Dispose()
}
}
function Test-ContainedPath {
param([string]$Path, [string]$Root)
try {
$full = [IO.Path]::GetFullPath($Path)
$base = [IO.Path]::GetFullPath($Root).TrimEnd([char[]]@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar))
} catch { return $false }
if ($full.Equals($base, [StringComparison]::OrdinalIgnoreCase)) { return $true }
return $full.StartsWith($base + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)
}
function Test-NoReparseAncestors {
param([string]$Path)
try { $full = [IO.Path]::GetFullPath($Path) } catch { return $false }
$cursorPath = $full
while (-not (Test-Path -LiteralPath $cursorPath)) {
$parent = Split-Path -Parent $cursorPath
if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $cursorPath) { break }
$cursorPath = $parent
}
$cursor = Get-Item -LiteralPath $cursorPath -Force -ErrorAction SilentlyContinue
while ($cursor) {
if ($cursor.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $false }
if ($cursor -is [IO.DirectoryInfo]) { $cursor = $cursor.Parent } else { $cursor = $cursor.Directory }
}
return $true
}
function Test-PriorStoreUnavailableError {
param($Record)
if ($null -eq $Record) { return $false }
if ($Record.CategoryInfo.Category -eq [Management.Automation.ErrorCategory]::PermissionDenied) { return $true }
$exception = $Record.Exception
while ($exception) {
if ($exception -is [UnauthorizedAccessException] -or $exception -is [IO.IOException]) { return $true }
$exception = $exception.InnerException
}
return $false
}
function Resolve-CodexHomeDirectory {
param([string]$Requested)
$hasRequested = -not [string]::IsNullOrWhiteSpace($Requested)
$hasEnvironment = -not [string]::IsNullOrWhiteSpace([string]$env:CODEX_HOME)
$source = if ($hasRequested) { 'explicit' } elseif ($hasEnvironment) { 'environment' } else { 'default' }
$candidate = if ($hasRequested) { $Requested } elseif ($hasEnvironment) { [string]$env:CODEX_HOME } else { Join-Path $HOME '.codex' }
try {
$full = [IO.Path]::GetFullPath($candidate)
if ($source -ne 'default') {
$item = Get-Item -LiteralPath $full -Force -ErrorAction Stop
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw 'supervision_codex_home_invalid'
}
$full = [IO.Path]::GetFullPath($item.FullName)
}
if (-not (Test-NoReparseAncestors $full)) { throw 'supervision_codex_home_invalid' }
} catch {
if ([string]$_.Exception.Message -eq 'supervision_codex_home_invalid') { throw }
if (Test-PriorStoreUnavailableError $_) { throw 'supervision_codex_home_unavailable' }
throw 'supervision_codex_home_invalid'
}
$full = $full.TrimEnd([char[]]@([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar))
if ([string]::IsNullOrWhiteSpace($full)) { throw 'supervision_codex_home_invalid' }
return [pscustomobject]@{
Path = $full
Identity = $full.ToUpperInvariant()
Source = $source
}
}
function Resolve-StatePath {
param([string]$Requested)
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
if ([string]::IsNullOrWhiteSpace($localAppData)) { throw 'supervision_state_path_invalid' }
$localRoot = [IO.Path]::GetFullPath((Join-Path $localAppData 'Chronos\Supervision'))
$priorTempRoot = [IO.Path]::GetFullPath((Join-Path $tempRoot 'Chronos\Supervision'))
$privateTempRoot = [IO.Path]::GetFullPath((Join-Path $tempRoot 'Chronos\Supervision-v2'))
$resolvedCodexHome = Resolve-CodexHomeDirectory $CodexHome
$codexHome = [string]$resolvedCodexHome.Path
$codexHomeIdentity = [string]$resolvedCodexHome.Identity
$script:CodexHomeSource = [string]$resolvedCodexHome.Source
$script:CodexHomeIdentity = (Get-TextHash $codexHomeIdentity).Substring(0, 16)
# Pre-CODEX_HOME state is globally scoped. Only an invocation using the true
# default home may import it; custom homes always start with isolated identity.
$script:AllowUnscopedLegacyMigration = $script:CodexHomeSource -eq 'default'
$scopeHash = Get-TextHash ('{0}|{1}' -f $env:COMPUTERNAME.ToUpperInvariant(), $codexHomeIdentity)
$priorScopeHash = Get-TextHash ('{0}|{1}' -f $env:COMPUTERNAME, $codexHome)
$script:DefaultInstallationScopeId = (Get-TextHash ('Chronos.Supervision.Installation.v3|{0}|{1}' -f $env:COMPUTERNAME.ToUpperInvariant(), $codexHomeIdentity)).Substring(0, 32)
$script:HookInboxDirectory = [IO.Path]::GetFullPath((Join-Path $tempRoot ('Chronos-Supervision-Inbox-v1-{0}' -f $scopeHash.Substring(0, 24))))
if (-not (Test-ContainedPath $script:HookInboxDirectory $tempRoot) -or -not (Test-NoReparseAncestors $script:HookInboxDirectory)) {
throw 'supervision_pending_event_path_invalid'
}
$script:DefaultStateCandidates = @(0..3 | ForEach-Object {
$slotRoot = [IO.Path]::GetFullPath((Join-Path $tempRoot ('Chronos-Supervision-v3-{0}-{1}' -f $scopeHash.Substring(0, 24), $_)))
Join-Path $slotRoot 'session-registry.json'
})
$script:LegacyDefaultStatePath = Join-Path $localRoot 'session-registry.json'
$script:LegacyInstallationScopePath = Join-Path $localRoot 'installation-scope.json'
$script:PriorDefaultStatePath = Join-Path $priorTempRoot 'session-registry.json'
$script:PriorInstallationScopePath = Join-Path $priorTempRoot 'installation-scope.json'
$script:PriorV2DefaultStatePath = Join-Path $privateTempRoot (Join-Path $priorScopeHash 'session-registry.json')
$script:PriorV2InstallationScopePath = Join-Path $privateTempRoot (Join-Path $priorScopeHash 'installation-scope.json')
$candidate = if ([string]::IsNullOrWhiteSpace($Requested)) {
$script:StateStoreMode = 'temp_private'
$script:StateStoreMigration = 'namespace_v3_new_root'
$script:StateStoreRecovery = 'bounded_default_slot_selection'
$script:DefaultStateCandidates[0]
} else {
$script:StateStoreMode = 'explicit'
try { [IO.Path]::GetFullPath($Requested) } catch { throw 'supervision_state_path_invalid' }
}
$defaultContained = @($script:DefaultStateCandidates | Where-Object {
[IO.Path]::GetFullPath($_).Equals([IO.Path]::GetFullPath($candidate), [StringComparison]::OrdinalIgnoreCase)
}).Count -eq 1
$contained = $defaultContained -or (Test-ContainedPath $candidate $localRoot) -or
(Test-ContainedPath $candidate $priorTempRoot) -or
(Test-ContainedPath $candidate $privateTempRoot)
if ([IO.Path]::GetExtension($candidate) -ne '.json' -or -not $contained) {
throw 'supervision_state_path_invalid'
}
if (-not (Test-NoReparseAncestors $candidate)) { throw 'supervision_state_path_invalid' }
return [IO.Path]::GetFullPath($candidate)
}
function Initialize-StateStore {
param([string]$ResolvedStatePath)
[string[]]$candidates = if ($script:StateStoreMode -eq 'temp_private') { @($script:DefaultStateCandidates) } else { @([string]$ResolvedStatePath) }
for ($slot = 0; $slot -lt $candidates.Count; $slot++) {
$candidate = [IO.Path]::GetFullPath([string]$candidates[$slot])
$directory = Split-Path -Parent $candidate
$probe = $null
try {
if (-not (Test-Path -LiteralPath $directory -PathType Container)) {
New-Item -ItemType Directory -Path $directory -Force -ErrorAction Stop | Out-Null
}
$item = Get-Item -LiteralPath $directory -Force -ErrorAction Stop
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-NoReparseAncestors $directory)) {
throw 'supervision_state_store_unwritable'
}
$probe = Join-Path $directory ('.supervision-probe-' + [guid]::NewGuid().ToString('N') + '.tmp')
$stream = New-Object IO.FileStream($probe, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try { $stream.Flush($true) } finally { $stream.Dispose() }
if ($script:StateStoreMode -eq 'temp_private') {
# Default-slot selection happens before the registry mutex exists. It
# must inspect prior state to recover from an inaccessible sandbox slot.
foreach ($existingPath in @($candidate, (Join-Path $directory 'installation-scope.json'))) {
if (-not (Test-Path -LiteralPath $existingPath -PathType Leaf)) { continue }
$existingItem = Get-Item -LiteralPath $existingPath -Force -ErrorAction Stop
if ($existingItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'supervision_state_store_unwritable' }
$readProbe = New-Object IO.FileStream($existingPath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
$readProbe.Dispose()
}
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
$existingState = Read-State $candidate -PreserveUnavailable
try {
Assert-StateProtectedIdentity $existingState
} catch {
if ([string]$_.Exception.Message -ne 'supervision_state_identity_unavailable') { throw }
$scopePath = Join-Path $directory 'installation-scope.json'
if ($null -eq $script:RecoveredV3InstallationScopeId -and (Test-Path -LiteralPath $scopePath -PathType Leaf)) {
try { $script:RecoveredV3InstallationScopeId = Read-InstallationScopeId $scopePath } catch {}
}
throw
}
}
}
$script:StateStoreWriteReady = $true
$script:StateStoreSlot = $slot
if ($script:StateStoreMode -eq 'temp_private') {
$script:StateStoreProtection = 'bounded_temp_slots_dpapi_ids'
if ($slot -gt 0) { $script:StateStoreMigration = 'default_state_slot_recovered' }
}
return $candidate
} catch {
$failureCode = [string]$_.Exception.Message
if ($script:StateStoreMode -ne 'temp_private') {
if ($failureCode -in @('supervision_state_invalid', 'supervision_state_identity_unavailable')) { throw 'supervision_state_invalid' }
throw 'supervision_state_store_unwritable'
}
if ($failureCode -eq 'supervision_state_invalid') { throw }
if ($failureCode -ne 'supervision_state_identity_unavailable' -and
$failureCode -ne 'supervision_state_store_unwritable' -and
-not (Test-PriorStoreUnavailableError $_)) {
throw 'supervision_state_store_unwritable'
}
} finally {
if ($probe) { Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue }
}
}
throw 'supervision_state_store_unwritable'
}
function Resolve-InstallationScopePath {
param([string]$ResolvedStatePath)
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
if ([string]::IsNullOrWhiteSpace($localAppData)) { throw 'supervision_install_scope_path_invalid' }
$localRoot = [IO.Path]::GetFullPath((Join-Path $localAppData 'Chronos\Supervision'))
$stateDirectory = Split-Path -Parent $ResolvedStatePath
$candidate = if (Test-ContainedPath $ResolvedStatePath $localRoot) {
Join-Path $localRoot 'installation-scope.json'
} else {
Join-Path $stateDirectory 'installation-scope.json'
}
if (-not (Test-NoReparseAncestors $candidate)) { throw 'supervision_install_scope_path_invalid' }
return [IO.Path]::GetFullPath($candidate)
}
function Read-InstallationScopeId {
param([string]$Path)
try {
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $item.Length -gt 256) { throw 'invalid' }
$bytes = [IO.File]::ReadAllBytes($Path)
$utf8 = [Text.UTF8Encoding]::new($false, $true)
$text = $utf8.GetString($bytes)
Assert-StrictJson $text 'supervision_install_scope_invalid'
$scope = ConvertTo-Hashtable ($text | ConvertFrom-Json -ErrorAction Stop)
Assert-ExactKeys $scope @('schema', 'id')
if (-not (Test-IsInteger $scope.schema) -or [int]$scope.schema -ne 1 -or [string]$scope.id -notmatch '^[a-f0-9]{32}$') { throw 'invalid' }
return [string]$scope.id
} catch {
if ([string]$_.Exception.Message -eq 'supervision_install_scope_path_invalid') { throw }
if (Test-PriorStoreUnavailableError $_) { throw }
throw 'supervision_install_scope_invalid'
}
}
function Write-InstallationScopeId {
param([string]$Path, [string]$Id)
if ($Id -notmatch '^[a-f0-9]{32}$') { throw 'supervision_install_scope_invalid' }
$directory = Split-Path -Parent $Path
if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
if (-not (Test-NoReparseAncestors $directory)) { throw 'supervision_install_scope_path_invalid' }
$json = ([ordered]@{ schema = 1; id = $Id } | ConvertTo-Json -Compress)
$temporary = Join-Path $directory ('.supervision-scope-' + [guid]::NewGuid().ToString('N') + '.tmp')
try {
$bytes = [Text.UTF8Encoding]::new($false).GetBytes($json)
$stream = New-Object IO.FileStream($temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None, 4096, [IO.FileOptions]::WriteThrough)
try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
try {
[IO.File]::Move($temporary, $Path)
} catch [IO.IOException] {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw 'supervision_install_scope_unavailable' }
}
} finally {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
}
return Read-InstallationScopeId $Path
}
function Get-OrCreateInstallationScopeId {
param([string]$ResolvedStatePath)
$path = Resolve-InstallationScopePath $ResolvedStatePath
if (Test-Path -LiteralPath $path -PathType Leaf) {
$script:InstallationScopeSource = if ($script:StateStoreMode -eq 'temp_private') { 'v3_anchor' } else { 'state_root_anchor' }
return Read-InstallationScopeId $path
}
if ($script:StateStoreMode -eq 'temp_private') {
if (-not [string]::IsNullOrWhiteSpace($script:RecoveredV3InstallationScopeId)) {
$script:InstallationScopeSource = 'recovered_v3_anchor'
return Write-InstallationScopeId $path $script:RecoveredV3InstallationScopeId
}
$priorScopePaths = @($script:PriorV2InstallationScopePath)
if ($script:AllowUnscopedLegacyMigration) {
$priorScopePaths += @($script:PriorInstallationScopePath, $script:LegacyInstallationScopePath)
}
foreach ($priorPath in $(if ($script:PriorMigrationBlocked) { @() } else { $priorScopePaths })) {
if ([string]::IsNullOrWhiteSpace($priorPath)) { continue }
try {
$priorItem = Get-Item -LiteralPath $priorPath -Force -ErrorAction Stop
if ($priorItem.PSIsContainer -or ($priorItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-NoReparseAncestors $priorPath)) {
$script:StateStoreMigration = 'prior_state_invalid_new_root'
$script:PriorStateDisposition = 'invalid_preserved'
$script:PriorMigrationBlocked = $true
break
}
$priorId = Read-InstallationScopeId $priorPath
if ($script:StateStoreMigration -in @('namespace_v3_new_root', 'default_state_slot_recovered')) {
$script:StateStoreMigration = 'prior_scope_imported'
}
$script:PriorStateDisposition = 'read_only_imported'
$script:InstallationScopeSource = 'prior_anchor_imported'
return Write-InstallationScopeId $path $priorId
} catch {
if (Test-PriorStoreUnavailableError $_) {
$script:StateStoreMigration = 'prior_state_unavailable_new_root'
$script:PriorStateDisposition = 'unavailable_preserved'
$script:PriorMigrationBlocked = $true
break
}
if ($_.CategoryInfo.Category -ne [Management.Automation.ErrorCategory]::ObjectNotFound) {
$script:StateStoreMigration = 'prior_state_invalid_new_root'
$script:PriorStateDisposition = 'invalid_preserved'
$script:PriorMigrationBlocked = $true
break
}
}
}
if ([string]::IsNullOrWhiteSpace($script:DefaultInstallationScopeId)) { throw 'supervision_install_scope_unavailable' }
$script:InstallationScopeSource = 'deterministic_host_codex_home_hash'
return Write-InstallationScopeId $path $script:DefaultInstallationScopeId
}
$directory = Split-Path -Parent $path
if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
if (-not (Test-NoReparseAncestors $directory)) { throw 'supervision_install_scope_path_invalid' }
$random = New-Object byte[] 16
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
try { $rng.GetBytes($random) } finally { $rng.Dispose() }
$id = ([BitConverter]::ToString($random)).Replace('-', '').ToLowerInvariant()
$script:InstallationScopeSource = 'state_root_anchor'
return Write-InstallationScopeId $path $id
}
function Normalize-OpaqueId {
param($Value, [string]$ErrorCode = 'supervision_session_id_invalid')
if (-not ($Value -is [string])) { throw $ErrorCode }
$text = $Value.Trim()
if ($text.Length -lt 1 -or $text.Length -gt 192 -or $text -notmatch '^[A-Za-z0-9._:/-]+$') { throw $ErrorCode }
if ($text -match '^[A-Za-z]:[\\/]' -or $text -match '^\\' -or ($text.StartsWith('/') -and -not $text.StartsWith('/root/'))) {
throw $ErrorCode
}
return $text
}
function Get-WorkspaceHash {
param($Value)
if (-not ($Value -is [string]) -or $Value.Length -lt 1 -or $Value.Length -gt 4096) {
return Get-TextHash 'workspace-unavailable'
}
try {
$normalized = [IO.Path]::GetFullPath($Value).TrimEnd([char[]]@('\\', '/')).ToUpperInvariant()
return Get-TextHash $normalized
} catch {
return Get-TextHash 'workspace-unavailable'
}
}
function Normalize-Model {
param($Value)
if ($Value -is [string]) {
$text = $Value.Trim()
if ($text.Length -ge 1 -and $text.Length -le 128 -and $text -match '^[A-Za-z0-9._:/-]+$') { return $text }
}
return 'unavailable'
}
function Protect-OpaqueId {
param([string]$Value)
try {
Add-Type -AssemblyName System.Security -ErrorAction SilentlyContinue
$bytes = [Text.Encoding]::UTF8.GetBytes($Value)
$protected = [Security.Cryptography.ProtectedData]::Protect(
$bytes,
$script:Entropy,
[Security.Cryptography.DataProtectionScope]::CurrentUser
)
return [Convert]::ToBase64String($protected)
} catch { throw 'supervision_crypto_unavailable' }
}
function Unprotect-OpaqueId {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value) -or $Value.Length -gt 1024) { throw 'supervision_state_invalid' }
try {
Add-Type -AssemblyName System.Security -ErrorAction SilentlyContinue
$plain = [Security.Cryptography.ProtectedData]::Unprotect(
[Convert]::FromBase64String($Value),
$script:Entropy,
[Security.Cryptography.DataProtectionScope]::CurrentUser
)
return Normalize-OpaqueId ([Text.Encoding]::UTF8.GetString($plain)) 'supervision_state_invalid'
} catch {
if ([string]$_.Exception.Message -eq 'supervision_state_invalid') { throw }
throw 'supervision_state_invalid'
}
}
function Test-ProtectedIdShape {
param($Value)
if (-not ($Value -is [string]) -or $Value.Length -lt 1 -or $Value.Length -gt 1024) { return $false }
try {
$bytes = [Convert]::FromBase64String($Value)
return $bytes.Length -ge 32 -and $bytes.Length -le 768
} catch { return $false }
}
function ConvertTo-UtcTimestamp {
param($Value)
if (-not ($Value -is [string]) -or $Value.Length -gt 40) { throw 'supervision_state_invalid' }
$parsed = [DateTimeOffset]::MinValue
if (-not [DateTimeOffset]::TryParse($Value, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$parsed)) {
throw 'supervision_state_invalid'
}
return $parsed.ToUniversalTime()
}
function New-State {
return [ordered]@{
schema = 5
revision = 0L
governor = $null
sessions = @{}
health = [ordered]@{
hookRuns = 0L
droppedEntries = 0L
ignoredStaleEvents = 0L
scanOffset = 0L
lastHookUtc = $null
turnSignals = 0L
duplicateSignals = 0L
processedHookEvents = @()
}
}
}
function Upgrade-State {
param($State)
$schema = Get-Value $State 'schema' $null
if ((Test-IsInteger $schema) -and [int]$schema -eq 2) {
foreach ($record in @($State.sessions.Values)) {
if (-not $record.Contains('generationHash')) { $record['generationHash'] = $null }
}
$State.schema = 3
}
if ((Test-IsInteger $State.schema) -and [int]$State.schema -eq 3) {
if (-not $State.health.Contains('turnSignals')) { $State.health['turnSignals'] = 0L }
if (-not $State.health.Contains('duplicateSignals')) { $State.health['duplicateSignals'] = 0L }
foreach ($record in @($State.sessions.Values)) {
if (-not $record.Contains('lastSignalHash')) { $record['lastSignalHash'] = $null }
if (-not $record.Contains('turnSignals')) { $record['turnSignals'] = 0L }
}
$State.schema = 4
}
if ((Test-IsInteger $State.schema) -and [int]$State.schema -eq 4) {
if (-not $State.health.Contains('processedHookEvents')) { $State.health['processedHookEvents'] = @() }
$State.schema = 5
}
return $State
}
function Assert-ExactKeys {
param($Record, [string[]]$Allowed)
if (-not ($Record -is [Collections.IDictionary])) { throw 'supervision_state_invalid' }
foreach ($key in $Record.Keys) {
if ($Allowed -notcontains [string]$key) { throw 'supervision_state_invalid' }
}
}
function Assert-StateProtectedIdentity {
param($State)
$protectedIds = [Collections.Generic.List[object]]::new()
if ($null -ne $State.governor) {
$protectedIds.Add([pscustomobject]@{ Hash = [string]$State.governor.idHash; ProtectedId = [string]$State.governor.protectedId }) | Out-Null
}
foreach ($key in @($State.sessions.Keys)) {
$protectedIds.Add([pscustomobject]@{ Hash = [string]$key; ProtectedId = [string]$State.sessions[$key].protectedId }) | Out-Null
}
foreach ($entry in $protectedIds) {
try { $plainId = Unprotect-OpaqueId $entry.ProtectedId } catch { throw 'supervision_state_identity_unavailable' }
if ((Get-TextHash $plainId) -ne $entry.Hash) { throw 'supervision_state_invalid' }
}
}
function Assert-State {
param($State, [switch]$ValidateProtectedIds)
Assert-ExactKeys $State @('schema', 'revision', 'governor', 'sessions', 'health')
if (-not (Test-IsInteger $State.schema) -or [int]$State.schema -ne 5 -or
-not (Test-IsInteger $State.revision) -or [long]$State.revision -lt 0 -or
-not ($State.sessions -is [Collections.IDictionary]) -or
-not ($State.health -is [Collections.IDictionary]) -or
$State.sessions.Count -gt $script:SessionLimit) {
throw 'supervision_state_invalid'
}
Assert-ExactKeys $State.health @('hookRuns', 'droppedEntries', 'ignoredStaleEvents', 'scanOffset', 'lastHookUtc', 'turnSignals', 'duplicateSignals', 'processedHookEvents')
if (-not (Test-IsInteger $State.health.hookRuns) -or [long]$State.health.hookRuns -lt 0 -or
-not (Test-IsInteger $State.health.droppedEntries) -or [long]$State.health.droppedEntries -lt 0 -or
-not (Test-IsInteger $State.health.ignoredStaleEvents) -or [long]$State.health.ignoredStaleEvents -lt 0 -or
-not (Test-IsInteger $State.health.scanOffset) -or [long]$State.health.scanOffset -lt 0 -or
-not (Test-IsInteger $State.health.turnSignals) -or [long]$State.health.turnSignals -lt 0 -or
-not (Test-IsInteger $State.health.duplicateSignals) -or [long]$State.health.duplicateSignals -lt 0) {
throw 'supervision_state_invalid'
}
if (-not ($State.health.processedHookEvents -is [Collections.IEnumerable]) -or
$State.health.processedHookEvents -is [string] -or
@($State.health.processedHookEvents).Count -gt $script:PendingEventReceiptLimit) {
throw 'supervision_state_invalid'
}
$processedHookEventSet = @{}
foreach ($receipt in @($State.health.processedHookEvents)) {
if (-not ($receipt -is [string]) -or [string]$receipt -notmatch '^[a-f0-9]{64}(\|[a-f0-9]{64})?$' -or $processedHookEventSet.ContainsKey([string]$receipt)) {
throw 'supervision_state_invalid'
}
$processedHookEventSet[[string]$receipt] = $true
}
if ($null -ne $State.health.lastHookUtc) { [void](ConvertTo-UtcTimestamp $State.health.lastHookUtc) }
if ($null -ne $State.governor) {
Assert-ExactKeys $State.governor @('idHash', 'protectedId', 'claimedAtUtc', 'lastSeenUtc', 'cycleCount', 'idleCycles', 'lastCycleUtc')
if ([string]$State.governor.idHash -notmatch '^[a-f0-9]{64}$') { throw 'supervision_state_invalid' }
if (-not (Test-ProtectedIdShape $State.governor.protectedId)) { throw 'supervision_state_invalid' }
[void](ConvertTo-UtcTimestamp $State.governor.claimedAtUtc)
[void](ConvertTo-UtcTimestamp $State.governor.lastSeenUtc)
if (-not (Test-IsInteger $State.governor.cycleCount) -or [long]$State.governor.cycleCount -lt 0 -or
-not (Test-IsInteger $State.governor.idleCycles) -or [long]$State.governor.idleCycles -lt 0) { throw 'supervision_state_invalid' }
if ($null -ne $State.governor.lastCycleUtc) { [void](ConvertTo-UtcTimestamp $State.governor.lastCycleUtc) }
}
foreach ($key in $State.sessions.Keys) {
if ([string]$key -notmatch '^[a-f0-9]{64}$') { throw 'supervision_state_invalid' }
$record = $State.sessions[$key]
Assert-ExactKeys $record @('idHash', 'protectedId', 'kind', 'parentHash', 'workspaceHash', 'model', 'state', 'source', 'generationHash', 'firstSeenUtc', 'lastSeenUtc', 'endedAtUtc', 'lastEventUtc', 'lastEventRank', 'recordRevision', 'lastSignalHash', 'turnSignals')
if ([string]$record.idHash -ne [string]$key -or -not (Test-ProtectedIdShape $record.protectedId)) { throw 'supervision_state_invalid' }
if ([string]$record.kind -notin @('task', 'agent') -or [string]$record.state -notin @('active', 'ended')) { throw 'supervision_state_invalid' }
if ($null -ne $record.parentHash -and [string]$record.parentHash -notmatch '^[a-f0-9]{64}$') { throw 'supervision_state_invalid' }
if ($null -ne $record.generationHash -and [string]$record.generationHash -notmatch '^[a-f0-9]{64}$') { throw 'supervision_state_invalid' }
if ($null -ne $record.lastSignalHash -and [string]$record.lastSignalHash -notmatch '^[a-f0-9]{64}$') { throw 'supervision_state_invalid' }
if ([string]$record.workspaceHash -notmatch '^[a-f0-9]{64}$' -or [string]$record.model -notmatch '^[A-Za-z0-9._:/-]{1,128}$') { throw 'supervision_state_invalid' }
if ([string]$record.source -notin @('startup', 'resume', 'clear', 'compact', 'subagent', 'fallback')) { throw 'supervision_state_invalid' }
[void](ConvertTo-UtcTimestamp $record.firstSeenUtc)
[void](ConvertTo-UtcTimestamp $record.lastSeenUtc)
if ($null -ne $record.endedAtUtc) { [void](ConvertTo-UtcTimestamp $record.endedAtUtc) }
[void](ConvertTo-UtcTimestamp $record.lastEventUtc)
if (-not (Test-IsInteger $record.lastEventRank) -or [int]$record.lastEventRank -lt 1 -or [int]$record.lastEventRank -gt 3) { throw 'supervision_state_invalid' }
if (-not (Test-IsInteger $record.recordRevision) -or [long]$record.recordRevision -lt 0 -or [long]$record.recordRevision -gt [long]$State.revision) { throw 'supervision_state_invalid' }
if (-not (Test-IsInteger $record.turnSignals) -or [long]$record.turnSignals -lt 0) { throw 'supervision_state_invalid' }
}
if ($null -ne $State.governor -and -not $State.sessions.Contains([string]$State.governor.idHash)) { throw 'supervision_state_invalid' }
if ($ValidateProtectedIds) { Assert-StateProtectedIdentity $State }
}
function Read-State {
param([string]$Path, [switch]$ValidateProtectedIds, [switch]$PreserveUnavailable)
if (-not (Test-Path -LiteralPath $Path)) { return New-State }
try {
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $item.Length -gt $script:StateByteLimit) { throw 'invalid' }
$bytes = [IO.File]::ReadAllBytes($Path)
$utf8 = New-Object Text.UTF8Encoding($false, $true)
$text = $utf8.GetString($bytes)
Assert-StrictJson $text 'supervision_state_invalid'
$state = Upgrade-State (ConvertTo-Hashtable ($text | ConvertFrom-Json -ErrorAction Stop))
Assert-State $state -ValidateProtectedIds:$ValidateProtectedIds
return $state
} catch {
if ($PreserveUnavailable -and (Test-PriorStoreUnavailableError $_)) { throw }
throw 'supervision_state_invalid'
}
}
function Write-State {
param($State, [string]$Path, [switch]$TrustedHookMutation)
# Hook state was fully validated immediately after the mutex was acquired and
# can only be changed by the closed lifecycle mutation functions below.
if (-not $TrustedHookMutation) { Assert-State $State }
$directory = Split-Path -Parent $Path
if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
if (-not (Test-NoReparseAncestors $directory)) { throw 'supervision_state_path_invalid' }
$temporary = Join-Path $directory ('.supervision-' + [guid]::NewGuid().ToString('N') + '.tmp')
$backup = Join-Path $directory ('.supervision-' + [guid]::NewGuid().ToString('N') + '.bak')
try {
$json = $State | ConvertTo-Json -Compress -Depth 12
$bytes = [Text.UTF8Encoding]::new($false).GetBytes($json)
if ($bytes.Length -gt $script:StateByteLimit) { throw 'supervision_state_capacity' }
# This registry is an advisory discovery cache that host liveness can rebuild.
# Atomic replacement matters; forcing a physical flush for every lifecycle
# hint only extends lock contention and increases disk work.
$stream = New-Object IO.FileStream($temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None, 4096, [IO.FileOptions]::None)
try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush() } finally { $stream.Dispose() }
if (-not $TrustedHookMutation) {
[void]((Get-Content -Raw -LiteralPath $temporary) | ConvertFrom-Json -ErrorAction Stop)
}
if (Test-Path -LiteralPath $Path -PathType Leaf) {
[IO.File]::Replace($temporary, $Path, $backup, $true)
} else {
[IO.File]::Move($temporary, $Path)
}
} catch {
if ([string]$_.Exception.Message -match '^supervision_(state_|pending_|install_)') { throw }
throw 'supervision_state_store_unwritable'
} finally {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
}
}
function Import-LegacyStateIfPresent {
param([string]$ResolvedStatePath)
if ($script:StateStoreMode -ne 'temp_private' -or (Test-Path -LiteralPath $ResolvedStatePath)) { return }
$priorStatePaths = @($script:PriorV2DefaultStatePath)
if ($script:AllowUnscopedLegacyMigration) {
$priorStatePaths += @($script:PriorDefaultStatePath, $script:LegacyDefaultStatePath)
}
foreach ($priorPath in $priorStatePaths) {
if ([string]::IsNullOrWhiteSpace($priorPath)) { continue }
try {
$priorItem = Get-Item -LiteralPath $priorPath -Force -ErrorAction Stop
if ($priorItem.PSIsContainer -or ($priorItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-NoReparseAncestors $priorPath)) {
$script:StateStoreMigration = 'prior_state_invalid_new_root'
$script:PriorStateDisposition = 'invalid_preserved'
$script:PriorMigrationBlocked = $true
return
}
$legacy = Read-State $priorPath -ValidateProtectedIds -PreserveUnavailable
Write-State $legacy $ResolvedStatePath
$script:StateStoreMigration = 'prior_state_imported'
$script:PriorStateDisposition = 'read_only_imported'
return
} catch {
if (Test-PriorStoreUnavailableError $_) {
$script:StateStoreMigration = 'prior_state_unavailable_new_root'
$script:PriorStateDisposition = 'unavailable_preserved'
$script:PriorMigrationBlocked = $true
return
}
if ($_.CategoryInfo.Category -ne [Management.Automation.ErrorCategory]::ObjectNotFound) {
$script:StateStoreMigration = 'prior_state_invalid_new_root'
$script:PriorStateDisposition = 'invalid_preserved'
$script:PriorMigrationBlocked = $true
return
}
}
}
}
function Get-PendingEventDirectory {
param([string]$ResolvedStatePath)
return $ResolvedStatePath + '.pending'
}
function New-PreparedHookEvent {
param($HookData, [DateTimeOffset]$EventObservedAt)
$event = [string](Get-Value $HookData 'hook_event_name' '')
if ($event -notin @('SessionStart', 'SessionEnd', 'SubagentStart', 'SubagentStop', 'Stop')) { throw 'supervision_hook_event_invalid' }
$session = Normalize-OpaqueId (Get-Value $HookData 'session_id')
$agent = $null
if ($event -in @('SubagentStart', 'SubagentStop')) {
$agent = Normalize-OpaqueId (Get-Value $HookData 'agent_id') 'supervision_agent_id_invalid'
}
$source = if ($event -eq 'SessionStart') { [string](Get-Value $HookData 'source' 'startup') } elseif ($event -in @('SubagentStart', 'SubagentStop')) { 'subagent' } else { 'fallback' }
if ($event -eq 'SessionStart' -and $source -notin @('startup', 'resume', 'clear', 'compact')) { throw 'supervision_hook_source_invalid' }
return [ordered]@{
schema = 2
event = $event
protectedSessionId = Protect-OpaqueId $session
protectedAgentId = if ($null -ne $agent) { Protect-OpaqueId $agent } else { $null }
workspaceHash = Get-WorkspaceHash (Get-Value $HookData 'cwd')
model = Normalize-Model (Get-Value $HookData 'model')
source = $source
signalHash = if ($event -eq 'Stop') { Get-TextHash (Normalize-OpaqueId (Get-Value $HookData 'turn_id') 'supervision_turn_id_invalid') } else { $null }
observedAtUtc = $EventObservedAt.ToString('o')
}
}
function Assert-PendingHookEvent {
param($Record)
$schema = Get-Value $Record 'schema' $null
if (-not (Test-IsInteger $schema) -or [int]$schema -notin @(1, 2)) { throw 'supervision_pending_event_invalid' }
if ([int]$schema -eq 1) {
Assert-ExactKeys $Record @('schema', 'event', 'protectedSessionId', 'protectedAgentId', 'workspaceHash', 'model', 'source', 'observedAtUtc')
} else {
Assert-ExactKeys $Record @('schema', 'event', 'protectedSessionId', 'protectedAgentId', 'workspaceHash', 'model', 'source', 'signalHash', 'observedAtUtc')
}
$allowedEvents = if ([int]$schema -eq 1) { @('SessionStart', 'SessionEnd', 'SubagentStart', 'SubagentStop') } else { @('SessionStart', 'SessionEnd', 'SubagentStart', 'SubagentStop', 'Stop') }
if ([string]$Record.event -notin $allowedEvents -or
-not (Test-ProtectedIdShape $Record.protectedSessionId) -or
[string]$Record.workspaceHash -notmatch '^[a-f0-9]{64}$' -or
[string]$Record.model -notmatch '^[A-Za-z0-9._:/-]{1,128}$' -or
[string]$Record.source -notin @('startup', 'resume', 'clear', 'compact', 'subagent', 'fallback')) {
throw 'supervision_pending_event_invalid'
}
if ([string]$Record.event -in @('SubagentStart', 'SubagentStop')) {
if (-not (Test-ProtectedIdShape $Record.protectedAgentId)) { throw 'supervision_pending_event_invalid' }
} elseif ($null -ne $Record.protectedAgentId) {
throw 'supervision_pending_event_invalid'
}
if ([int]$schema -eq 2 -and [string]$Record.event -eq 'Stop') {
if ([string]$Record.signalHash -notmatch '^[a-f0-9]{64}$') { throw 'supervision_pending_event_invalid' }
} elseif ([int]$schema -eq 2 -and $null -ne $Record.signalHash) {
throw 'supervision_pending_event_invalid'
}
[void](ConvertTo-UtcTimestamp $Record.observedAtUtc)
}
function Write-PendingHookEvent {
param([string]$ResolvedStatePath, $PreparedEvent)
Assert-PendingHookEvent $PreparedEvent
$directory = Get-PendingEventDirectory $ResolvedStatePath
if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
$directoryItem = Get-Item -LiteralPath $directory -Force
if (-not $directoryItem.PSIsContainer -or ($directoryItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-NoReparseAncestors $directory)) { throw 'supervision_pending_event_path_invalid' }
$bytes = [Text.UTF8Encoding]::new($false).GetBytes(($PreparedEvent | ConvertTo-Json -Compress -Depth 4))
if ($bytes.Length -gt $script:PendingEventByteLimit) { throw 'supervision_pending_event_capacity' }
foreach ($staleReservation in @(Get-ChildItem -LiteralPath $directory -File -Force -Filter 'pending-slot-*.lock' | Select-Object -First $script:PendingEventLimit)) {
if ($staleReservation.LastWriteTimeUtc -lt [DateTime]::UtcNow.AddMinutes(-5)) {
Remove-Item -LiteralPath $staleReservation.FullName -Force -ErrorAction SilentlyContinue
}
}
$start = [int]([BitConverter]::ToUInt32(([guid]::NewGuid()).ToByteArray(), 0) % [uint32]$script:PendingEventLimit)
$reservation = $null
$final = $null
for ($offset = 0; $offset -lt $script:PendingEventLimit; $offset++) {
$slot = ($start + $offset) % $script:PendingEventLimit
$candidateFinal = Join-Path $directory ('pending-slot-{0:d3}.json' -f $slot)
$candidateReservation = Join-Path $directory ('pending-slot-{0:d3}.lock' -f $slot)
if (Test-Path -LiteralPath $candidateFinal -PathType Leaf) { continue }
try {
$reserveStream = New-Object IO.FileStream($candidateReservation, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
$reserveStream.Dispose()
if (Test-Path -LiteralPath $candidateFinal -PathType Leaf) {
Remove-Item -LiteralPath $candidateReservation -Force -ErrorAction SilentlyContinue
continue
}
$reservation = $candidateReservation
$final = $candidateFinal
break
} catch [IO.IOException] { continue }
}
if ([string]::IsNullOrWhiteSpace($final)) { throw 'supervision_pending_event_capacity' }
$temporary = Join-Path $directory ('.pending-' + [guid]::NewGuid().ToString('N') + '.tmp')
try {
$stream = New-Object IO.FileStream($temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
[IO.File]::Move($temporary, $final)
} finally {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
if ($reservation) { Remove-Item -LiteralPath $reservation -Force -ErrorAction SilentlyContinue }
}
}
function Write-PendingHookEventDurably {
param([string]$ResolvedStatePath, $PreparedEvent)
$lastError = $null
for ($attempt = 1; $attempt -le $script:PendingEventWriteAttemptLimit; $attempt++) {
try {
Write-PendingHookEvent $ResolvedStatePath $PreparedEvent
return
} catch {
$lastError = $_
if ($attempt -lt $script:PendingEventWriteAttemptLimit) {
[Threading.Thread]::Sleep($script:PendingEventRetryDelayMilliseconds)
}
}
}
if ($null -ne $lastError -and [string]$lastError.Exception.Message -match '^supervision_[a-z_]+$') {
throw ([string]$lastError.Exception.Message)
}
throw 'supervision_pending_event_unwritable'
}
function Read-PendingHookEvents {
param([string]$ResolvedStatePath)
$result = [Collections.Generic.List[object]]::new()
$files = [Collections.Generic.List[object]]::new()
$presentPathHashes = [Collections.Generic.List[string]]::new()
$directories = @((Get-PendingEventDirectory $ResolvedStatePath))
if ($script:StateStoreMode -eq 'temp_private' -and -not [string]::IsNullOrWhiteSpace([string]$script:HookInboxDirectory)) {
$directories += @([string]$script:HookInboxDirectory)
}
foreach ($directory in @($directories | Select-Object -Unique)) {
if (-not (Test-Path -LiteralPath $directory)) { continue }
$directoryItem = Get-Item -LiteralPath $directory -Force
if (-not $directoryItem.PSIsContainer -or ($directoryItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-NoReparseAncestors $directory)) { throw 'supervision_pending_event_path_invalid' }
foreach ($file in @(Get-ChildItem -LiteralPath $directory -File -Force -Filter 'pending-*.json' | Sort-Object Name)) {
$pathHash = Get-TextHash ('pending-path|{0}' -f $file.FullName.ToUpperInvariant())
$presentPathHashes.Add($pathHash) | Out-Null
$files.Add([pscustomobject]@{ File = $file; PathHash = $pathHash }) | Out-Null
if ($files.Count -gt $script:PendingEventReceiptLimit) { throw 'supervision_pending_event_capacity' }
}
}
foreach ($candidate in @($files | Select-Object -First $script:PendingEventLimit)) {
$file = $candidate.File
$pathHash = [string]$candidate.PathHash
$eventHash = Get-TextHash ('pending-invalid|{0}|{1}|{2}' -f $pathHash, $file.Length, $file.LastWriteTimeUtc.Ticks)
try {
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $file.Length -gt $script:PendingEventByteLimit) { throw 'invalid' }
try { $bytes = [IO.File]::ReadAllBytes($file.FullName) } catch {
$result.Add([pscustomobject]@{ Path = $file.FullName; PathHash = $pathHash; EventHash = $null; Record = $null; SessionId = $null; AgentId = $null; ObservedAt = $null; Readable = $false; Valid = $false }) | Out-Null
continue
}
$eventHash = Get-BytesHash $bytes
$text = [Text.UTF8Encoding]::new($false, $true).GetString($bytes)
Assert-StrictJson $text 'supervision_pending_event_invalid'
$record = ConvertTo-Hashtable ($text | ConvertFrom-Json -ErrorAction Stop)
Assert-PendingHookEvent $record
$session = Unprotect-OpaqueId $record.protectedSessionId
$agent = $null
if ([string]$record.event -in @('SubagentStart', 'SubagentStop')) { $agent = Unprotect-OpaqueId $record.protectedAgentId }
$observedAt = ConvertTo-UtcTimestamp $record.observedAtUtc
$result.Add([pscustomobject]@{
Path = $file.FullName
PathHash = $pathHash
EventHash = $eventHash
Record = $record
SessionId = $session
AgentId = $agent
ObservedAt = $observedAt
Readable = $true
Valid = $true
}) | Out-Null
} catch {
$result.Add([pscustomobject]@{ Path = $file.FullName; PathHash = $pathHash; EventHash = $eventHash; Record = $null; SessionId = $null; AgentId = $null; ObservedAt = $null; Readable = $true; Valid = $false }) | Out-Null
}
}
return [pscustomobject]@{ Items = @($result); PresentPathHashes = @($presentPathHashes) }
}
function Merge-PendingHookEvents {
param($State, [string]$ResolvedStatePath, [DateTimeOffset]$Now)
$snapshot = Read-PendingHookEvents $ResolvedStatePath
$items = @($snapshot.Items)
$presentPathHashes = @{}
foreach ($pathHash in @($snapshot.PresentPathHashes)) { $presentPathHashes[[string]$pathHash] = $true }
$processedByPath = @{}
$legacyEventHashes = @{}
foreach ($receipt in @($State.health.processedHookEvents)) {
$parts = @(([string]$receipt) -split '\|', 2)
if ($parts.Count -eq 1) {
if ($presentPathHashes.Count -gt 0) { $legacyEventHashes[[string]$parts[0]] = $true }
} elseif ($presentPathHashes.ContainsKey([string]$parts[0])) {
$processedByPath[[string]$parts[0]] = [string]$parts[1]
}
}
$valid = @($items | Where-Object { $_.Readable -and $_.Valid } | Sort-Object ObservedAt, Path)
foreach ($item in $valid) {
$pathHash = [string]$item.PathHash
$eventHash = [string]$item.EventHash
if ($processedByPath.ContainsKey($pathHash) -and [string]$processedByPath[$pathHash] -eq $eventHash) { continue }
if ($legacyEventHashes.ContainsKey($eventHash)) {
$legacyEventHashes.Remove($eventHash)
$processedByPath[$pathHash] = $eventHash
continue
}
$record = $item.Record
$hookData = @{
hook_event_name = [string]$record.event
session_id = [string]$item.SessionId
model = [string]$record.model
source = [string]$record.source
}
$preparedProtectedId = [string]$record.protectedSessionId
if ([string]$record.event -in @('SubagentStart', 'SubagentStop')) {
$hookData.agent_id = [string]$item.AgentId
$preparedProtectedId = [string]$record.protectedAgentId
}
Invoke-HookEvent $State $hookData $Now $item.ObservedAt $preparedProtectedId ([string]$record.workspaceHash) ([string](Get-Value $record 'signalHash' ''))
$processedByPath[$pathHash] = $eventHash
}
foreach ($item in @($items | Where-Object { $_.Readable -and -not $_.Valid })) {
$pathHash = [string]$item.PathHash
$eventHash = [string]$item.EventHash
if ($processedByPath.ContainsKey($pathHash) -and [string]$processedByPath[$pathHash] -eq $eventHash) { continue }
if ($legacyEventHashes.ContainsKey($eventHash)) {
$legacyEventHashes.Remove($eventHash)
$processedByPath[$pathHash] = $eventHash
continue
}
$State.health.droppedEntries = [long]$State.health.droppedEntries + 1
$processedByPath[$pathHash] = $eventHash
}
$processedReceipts = [Collections.Generic.List[string]]::new()
foreach ($eventHash in @($legacyEventHashes.Keys | Sort-Object)) { $processedReceipts.Add([string]$eventHash) | Out-Null }
foreach ($pathHash in @($processedByPath.Keys | Sort-Object)) {
$processedReceipts.Add(('{0}|{1}' -f $pathHash, [string]$processedByPath[$pathHash])) | Out-Null
}
if ($processedReceipts.Count -gt $script:PendingEventReceiptLimit) { throw 'supervision_pending_event_capacity' }
$State.health.processedHookEvents = @($processedReceipts)
return @($items | Where-Object { $_.Readable })
}
function Remove-PendingHookEvents {
param($Items, [string]$ResolvedStatePath)
# Keep the empty parent directory. Fallback writers do not hold the registry
# mutex, so deleting it here can race a writer between its path check and write.
foreach ($item in @($Items)) {
try { Remove-Item -LiteralPath ([string]$item.Path) -Force -ErrorAction Stop } catch {}
}
}
function Read-HostInventory {
param([string]$Path, [DateTimeOffset]$Now)
if ([string]::IsNullOrWhiteSpace($Path)) { throw 'supervision_host_inventory_required' }
try { $full = [IO.Path]::GetFullPath($Path) } catch { throw 'supervision_host_inventory_invalid' }
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
if ([IO.Path]::GetExtension($full) -ne '.json' -or -not (Test-ContainedPath $full $tempRoot) -or -not (Test-NoReparseAncestors $full)) {
throw 'supervision_host_inventory_invalid'
}
try {
$item = Get-Item -LiteralPath $full -Force
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $item.Length -gt $script:HostInventoryByteLimit) { throw 'invalid' }
$bytes = [IO.File]::ReadAllBytes($full)
$text = [Text.UTF8Encoding]::new($false, $true).GetString($bytes)
Assert-StrictJson $text 'supervision_host_inventory_invalid'
$inventory = ConvertTo-Hashtable ($text | ConvertFrom-Json -ErrorAction Stop)
Assert-ExactKeys $inventory @('schemaVersion', 'capturedAtUtc', 'complete', 'callerVisibility', 'tasks')
if (-not (Test-IsInteger $inventory.schemaVersion) -or [int]$inventory.schemaVersion -notin @(1, 2) -or -not ($inventory.complete -is [bool])) { throw 'invalid' }
$schemaVersion = [int]$inventory.schemaVersion
$callerVisibility = 'included'
if ($schemaVersion -eq 1) {
if ($inventory.Contains('callerVisibility')) { throw 'invalid' }
} else {
if (-not $inventory.Contains('callerVisibility') -or [string]$inventory.callerVisibility -notin @('included', 'excluded_by_host')) { throw 'invalid' }
$callerVisibility = [string]$inventory.callerVisibility
}
$captured = ConvertTo-UtcTimestamp $inventory.capturedAtUtc
if ($captured -gt $Now.AddMinutes(5) -or $captured -lt $Now.AddMinutes(-15)) { throw 'invalid' }
$tasks = @($inventory.tasks)
if ($tasks.Count -gt $script:SessionLimit) { throw 'invalid' }
$seen = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal)
$normalized = [Collections.Generic.List[object]]::new()
$activeStates = @('active', 'running', 'waiting', 'blocked', 'needs_attention', 'pending', 'idle', 'ready')
$endedStates = @('completed', 'failed', 'cancelled', 'archived', 'ended')
foreach ($task in $tasks) {
Assert-ExactKeys $task @('id', 'status', 'generation')
$id = Normalize-OpaqueId (Get-Value $task 'id') 'supervision_host_inventory_invalid'
if (-not $seen.Add($id)) { throw 'invalid' }
$status = [string](Get-Value $task 'status' '')
if ($status -notin @($activeStates + $endedStates)) { throw 'invalid' }
$generation = $null
if ($task.Contains('generation') -and $null -ne $task.generation) {
$generation = Normalize-OpaqueId $task.generation 'supervision_host_inventory_invalid'
}
$normalized.Add([pscustomobject]@{
Id = $id
Active = ($status -in $activeStates)
Status = if ($status -in $activeStates) { 'live' } else { 'ended' }
Generation = $generation
}) | Out-Null
}
return [pscustomobject]@{
SchemaVersion = $schemaVersion
CapturedAt = $captured
Complete = [bool]$inventory.complete
CallerVisibility = $callerVisibility
RawObserved = $normalized.Count
Tasks = @($normalized)
}
} catch {
if ([string]$_.Exception.Message -match '^supervision_host_inventory_') { throw }
throw 'supervision_host_inventory_invalid'
}
}
function Complete-HostInventoryForGovernor {
param($Inventory, [string]$CurrentGovernorId)
$governorCount = @($Inventory.Tasks | Where-Object { [string]$_.Id -eq $CurrentGovernorId }).Count
$tasks = @($Inventory.Tasks)
$governorSource = 'inventory'
if ([string]$Inventory.CallerVisibility -eq 'included') {
if ($governorCount -ne 1) { throw 'supervision_governor_not_in_host_inventory' }
} elseif ([string]$Inventory.CallerVisibility -eq 'excluded_by_host') {
if ($governorCount -ne 0) { throw 'supervision_host_inventory_invalid' }
$tasks += [pscustomobject]@{
Id = $CurrentGovernorId
Active = $true
Status = 'live'
Generation = $null
}
$governorSource = 'intrinsic_cycle_caller'
} else {
throw 'supervision_host_inventory_invalid'
}
return [pscustomobject]@{
SchemaVersion = [int]$Inventory.SchemaVersion
CapturedAt = $Inventory.CapturedAt
Complete = [bool]$Inventory.Complete
CallerVisibility = [string]$Inventory.CallerVisibility
GovernorSource = $governorSource
RawObserved = [int]$Inventory.RawObserved
Tasks = @($tasks)
}
}
function Invoke-HostInventoryReconciliation {
param($State, $Inventory, [string]$CurrentGovernorId, [DateTimeOffset]$Now)
$currentHash = Get-TextHash $CurrentGovernorId
if ($null -eq $State.governor -or [string]$State.governor.idHash -ne $currentHash) { throw 'supervision_governor_mismatch' }
$present = @{}
$added = 0
$reactivated = 0
$generationChanged = 0
$ended = 0
foreach ($task in @($Inventory.Tasks)) {
$hash = Get-TextHash ([string]$task.Id)
$generationHash = if ([string]::IsNullOrWhiteSpace([string]$task.Generation)) { $null } else { Get-TextHash ([string]$task.Generation) }
$present[$hash] = $true
$existing = if ($State.sessions.Contains($hash)) { $State.sessions[$hash] } else { $null }
if ([bool]$task.Active) {
if ($null -eq $existing) {
if (Set-SessionRecord $State ([string]$task.Id) 'task' $null (Get-TextHash 'workspace-unavailable') 'unavailable' 'active' 'fallback' $Now $Inventory.CapturedAt 3 -GenerationHash $generationHash -AllowReactivation) { $added++ }
} elseif ([string]$existing.state -eq 'ended') {
if (Set-SessionRecord $State ([string]$task.Id) 'task' $null ([string]$existing.workspaceHash) ([string]$existing.model) 'active' ([string]$existing.source) $Now $Inventory.CapturedAt 3 -GenerationHash $generationHash -AllowReactivation) { $reactivated++ }
} else {
$priorGeneration = [string](Get-Value $existing 'generationHash' '')
if (Set-SessionRecord $State ([string]$task.Id) 'task' $null ([string]$existing.workspaceHash) ([string]$existing.model) 'active' ([string]$existing.source) $Now $Inventory.CapturedAt 3 -GenerationHash $generationHash -AllowReactivation) {
if ($generationHash -and $priorGeneration -and $generationHash -ne $priorGeneration) { $generationChanged++ }
}
}
} elseif ($null -ne $existing -and [string]$existing.state -eq 'active' -and $hash -ne $currentHash) {
[void](Set-RecordEndedByHash $State $hash $Now $Inventory.CapturedAt $generationHash)
if ([string]$State.sessions[$hash].state -eq 'ended') { $ended++ }
}
}
if ([bool]$Inventory.Complete) {
foreach ($hash in @($State.sessions.Keys)) {
$record = $State.sessions[$hash]
if ([string]$record.kind -ne 'task' -or [string]$record.state -ne 'active' -or $hash -eq $currentHash -or $present.ContainsKey($hash)) { continue }
[void](Set-RecordEndedByHash $State ([string]$hash) $Now $Inventory.CapturedAt)
if ([string]$State.sessions[$hash].state -eq 'ended') { $ended++ }
}
}
return [pscustomobject]@{ Added = $added; Reactivated = $reactivated; GenerationChanged = $generationChanged; Ended = $ended; Observed = @($Inventory.Tasks).Count; Complete = [bool]$Inventory.Complete }
}
function Get-CompactHostTaskStatuses {
param($Inventory)
@($Inventory.Tasks | ForEach-Object {
[ordered]@{
idHash = (Get-TextHash ([string]$_.Id)).Substring(0, 16)
status = [string]$_.Status
generation = if ([string]::IsNullOrWhiteSpace([string]$_.Generation)) { 'unavailable' } else { 'observed' }
}
})
}
function New-RegistryMutex {
param([string]$Path)
$pathHash = Get-TextHash ([IO.Path]::GetFullPath($Path).ToUpperInvariant())
$script:RegistryMutexIdentity = $pathHash.Substring(0, 16)
$suffix = 'ChronosSupervision-' + $pathHash.Substring(0, 24)
$sid = $null
$security = $null
try {
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
$security = New-Object Security.AccessControl.MutexSecurity
$rule = New-Object Security.AccessControl.MutexAccessRule($sid, [Security.AccessControl.MutexRights]::FullControl, [Security.AccessControl.AccessControlType]::Allow)
$security.AddAccessRule($rule)
} catch { throw 'supervision_mutex_unavailable' }
try {
$created = $false
return New-Object Threading.Mutex($false, ('Global\' + $suffix), [ref]$created, $security)
} catch { throw 'supervision_mutex_unavailable' }
}
function Remove-ExpiredRecords {
param($State, [DateTimeOffset]$Now)
$remove = [Collections.Generic.List[string]]::new()
foreach ($key in @($State.sessions.Keys)) {
if ($null -ne $State.governor -and [string]$State.governor.idHash -eq [string]$key) { continue }
$record = $State.sessions[$key]
$lastSeen = ConvertTo-UtcTimestamp $record.lastSeenUtc
if (($record.state -eq 'ended' -and ($Now - $lastSeen).TotalHours -ge $script:EndedRetentionHours) -or
($record.state -eq 'active' -and ($Now - $lastSeen).TotalDays -ge $script:ActiveRetentionDays)) {
$remove.Add([string]$key) | Out-Null
}
}
foreach ($key in $remove) { $State.sessions.Remove($key) }
}
function Set-RecordEndedByHash {
param($State, [string]$Hash, [DateTimeOffset]$Now, [DateTimeOffset]$EventObservedAt, [string]$GenerationHash)
if (-not $State.sessions.Contains($Hash)) { return $false }
$record = $State.sessions[$Hash]
if ($record.state -eq 'ended') { return $true }
if ($GenerationHash -and $record.generationHash -and [string]$record.generationHash -ne $GenerationHash) {
$State.health.ignoredStaleEvents = [long]$State.health.ignoredStaleEvents + 1
return $false
}
$lastEvent = ConvertTo-UtcTimestamp $record.lastEventUtc
if ($EventObservedAt -lt $lastEvent -or ($EventObservedAt -eq $lastEvent -and 2 -le [int]$record.lastEventRank)) {
$State.health.ignoredStaleEvents = [long]$State.health.ignoredStaleEvents + 1
return $false
}
$State.revision = [long]$State.revision + 1
$record.state = 'ended'
$record.lastSeenUtc = $Now.ToString('o')
$record.endedAtUtc = $Now.ToString('o')
$record.lastEventUtc = $EventObservedAt.ToString('o')
$record.lastEventRank = 2
$record.recordRevision = [long]$State.revision
return $true
}
function Set-SessionRecord {
param(
$State,
[string]$Id,
[string]$Kind,
[string]$ParentHash,
[string]$WorkspaceHash,
[string]$Model,
[string]$LifecycleState,
[string]$Source,
[DateTimeOffset]$Now,
[DateTimeOffset]$EventObservedAt,
[ValidateRange(1, 3)][int]$EventRank,
[string]$PreparedProtectedId,
[string]$GenerationHash,
[switch]$AllowReactivation
)
$hash = Get-TextHash $Id
$existing = if ($State.sessions.Contains($hash)) { $State.sessions[$hash] } else { $null }
if ($null -ne $existing) {
$lastEvent = ConvertTo-UtcTimestamp $existing.lastEventUtc
if ($EventObservedAt -lt $lastEvent -or
($EventObservedAt -eq $lastEvent -and $EventRank -le [int]$existing.lastEventRank) -or
($existing.state -eq 'ended' -and $LifecycleState -eq 'active' -and -not $AllowReactivation)) {
$State.health.ignoredStaleEvents = [long]$State.health.ignoredStaleEvents + 1
return $false
}
}
if ($null -eq $existing -and $State.sessions.Count -ge $script:SessionLimit) {
Remove-ExpiredRecords $State $Now
if ($State.sessions.Count -ge $script:SessionLimit) {
$State.health.droppedEntries = [long]$State.health.droppedEntries + 1
return $false
}
}
$State.revision = [long]$State.revision + 1
$firstSeen = if ($null -ne $existing) { [string]$existing.firstSeenUtc } else { $Now.ToString('o') }
$State.sessions[$hash] = [ordered]@{
idHash = $hash
protectedId = if ($null -ne $existing) {
[string]$existing.protectedId
} elseif (-not [string]::IsNullOrWhiteSpace($PreparedProtectedId)) {
$PreparedProtectedId
} else {
Protect-OpaqueId $Id
}
kind = $Kind
parentHash = if ([string]::IsNullOrWhiteSpace($ParentHash)) { $null } else { $ParentHash }
workspaceHash = $WorkspaceHash
model = $Model
state = $LifecycleState
source = $Source
generationHash = if (-not [string]::IsNullOrWhiteSpace($GenerationHash)) { $GenerationHash } elseif ($existing) { $existing.generationHash } else { $null }
firstSeenUtc = $firstSeen
lastSeenUtc = $Now.ToString('o')
endedAtUtc = if ($LifecycleState -eq 'ended') { $Now.ToString('o') } else { $null }
lastEventUtc = $EventObservedAt.ToString('o')
lastEventRank = $EventRank
recordRevision = [long]$State.revision
lastSignalHash = if ($existing) { $existing.lastSignalHash } else { $null }
turnSignals = if ($existing) { [long]$existing.turnSignals } else { 0L }
}
if ($null -ne $State.governor -and [string]$State.governor.idHash -eq $hash) {
$State.governor.lastSeenUtc = $Now.ToString('o')
}
return $true
}
function Invoke-HookEvent {
param($State, $HookData, [DateTimeOffset]$Now, [DateTimeOffset]$EventObservedAt, [string]$PreparedProtectedId, [string]$PreparedWorkspaceHash, [string]$PreparedSignalHash)
$event = [string](Get-Value $HookData 'hook_event_name' '')
if ($event -notin @('SessionStart', 'SessionEnd', 'SubagentStart', 'SubagentStop', 'Stop')) { throw 'supervision_hook_event_invalid' }
$session = Normalize-OpaqueId (Get-Value $HookData 'session_id')
$workspaceHash = if ([string]$PreparedWorkspaceHash -match '^[a-f0-9]{64}$') { $PreparedWorkspaceHash } else { Get-WorkspaceHash (Get-Value $HookData 'cwd') }
$model = Normalize-Model (Get-Value $HookData 'model')
switch ($event) {
'SessionStart' {
$source = [string](Get-Value $HookData 'source' 'startup')
if ($source -notin @('startup', 'resume', 'clear', 'compact')) { throw 'supervision_hook_source_invalid' }
[void](Set-SessionRecord $State $session 'task' $null $workspaceHash $model 'active' $source $Now $EventObservedAt 1 $PreparedProtectedId)
}
'SessionEnd' {
$hash = Get-TextHash $session
$source = if ($State.sessions.Contains($hash)) { [string]$State.sessions[$hash].source } else { 'fallback' }
$sessionEnded = Set-SessionRecord $State $session 'task' $null $workspaceHash $model 'ended' $source $Now $EventObservedAt 2 $PreparedProtectedId
if ($sessionEnded) {
foreach ($key in @($State.sessions.Keys)) {
$record = $State.sessions[$key]
if ($record.kind -eq 'agent' -and $record.parentHash -eq $hash -and $record.state -eq 'active') {
Set-RecordEndedByHash $State ([string]$key) $Now $EventObservedAt
}
}
}
}
'SubagentStart' {
$agent = Normalize-OpaqueId (Get-Value $HookData 'agent_id') 'supervision_agent_id_invalid'
$parentHash = Get-TextHash $session
$parentEnded = $State.sessions.Contains($parentHash) -and [string]$State.sessions[$parentHash].state -eq 'ended'
$agentHash = Get-TextHash $agent
if (-not $parentEnded) {
[void](Set-SessionRecord $State $agent 'agent' $parentHash $workspaceHash $model 'active' 'subagent' $Now $EventObservedAt 1 $PreparedProtectedId)
} elseif ($State.sessions.Contains($agentHash) -and [string]$State.sessions[$agentHash].state -eq 'ended') {
# Reapply the delayed start as active so the terminal-state guard records
# it as stale without rewriting the existing tombstone.
[void](Set-SessionRecord $State $agent 'agent' $parentHash $workspaceHash $model 'active' 'subagent' $Now $EventObservedAt 1 $PreparedProtectedId)
} else {
[void](Set-SessionRecord $State $agent 'agent' $parentHash $workspaceHash $model 'ended' 'subagent' $Now $EventObservedAt 2 $PreparedProtectedId)
}
}
'SubagentStop' {
$agent = Normalize-OpaqueId (Get-Value $HookData 'agent_id') 'supervision_agent_id_invalid'
$hash = Get-TextHash $agent
$record = if ($State.sessions.Contains($hash)) { $State.sessions[$hash] } else { $null }
$recordWorkspaceHash = if ($record) { [string]$record.workspaceHash } else { $workspaceHash }
$recordModel = if ($record) { [string]$record.model } else { $model }
[void](Set-SessionRecord $State $agent 'agent' (Get-TextHash $session) $recordWorkspaceHash $recordModel 'ended' 'subagent' $Now $EventObservedAt 2 $PreparedProtectedId)
}
'Stop' {
$signalHash = if ($PreparedSignalHash -match '^[a-f0-9]{64}$') {
$PreparedSignalHash
} else {
Get-TextHash (Normalize-OpaqueId (Get-Value $HookData 'turn_id') 'supervision_turn_id_invalid')
}
$hash = Get-TextHash $session
if ($State.sessions.Contains($hash) -and [string]$State.sessions[$hash].lastSignalHash -eq $signalHash) {
$State.health.duplicateSignals = [long]$State.health.duplicateSignals + 1
} else {
$existing = if ($State.sessions.Contains($hash)) { $State.sessions[$hash] } else { $null }
$source = if ($existing) { [string]$existing.source } else { 'fallback' }
$updated = Set-SessionRecord $State $session 'task' $null $workspaceHash $model 'active' $source $Now $EventObservedAt 3 $PreparedProtectedId
if ($updated) {
$State.sessions[$hash].lastSignalHash = $signalHash
$State.sessions[$hash].turnSignals = [long]$State.sessions[$hash].turnSignals + 1
$State.health.turnSignals = [long]$State.health.turnSignals + 1
}
}
}
}
$State.health.hookRuns = [long]$State.health.hookRuns + 1
$State.health.lastHookUtc = $Now.ToString('o')
Remove-ExpiredRecords $State $Now
}
function Get-DiscoveryPayload {
param($State, [string]$RequestedAction, [string]$CurrentSession, [long]$Cursor, [DateTimeOffset]$Now)
$governorId = $null
if ($null -ne $State.governor) { $governorId = Unprotect-OpaqueId $State.governor.protectedId }
$currentHash = if ([string]::IsNullOrWhiteSpace($CurrentSession)) { $null } else { Get-TextHash (Normalize-OpaqueId $CurrentSession) }
$activeTasks = [Collections.Generic.List[object]]::new()
$activeAgents = [Collections.Generic.List[object]]::new()
$allActive = [Collections.Generic.List[object]]::new()
$changes = [Collections.Generic.List[object]]::new()
$governorHash = if ($null -ne $State.governor) { [string]$State.governor.idHash } else { $null }
foreach ($key in @($State.sessions.Keys | Sort-Object)) {
$record = $State.sessions[$key]
$rawId = Unprotect-OpaqueId $record.protectedId
$summary = [ordered]@{
taskId = $rawId
idHash = ([string]$record.idHash).Substring(0, 16)
kind = [string]$record.kind
state = [string]$record.state
workspaceHash = ([string]$record.workspaceHash).Substring(0, 16)
model = [string]$record.model
lastSeenUtc = [string]$record.lastSeenUtc
turnSignals = [long]$record.turnSignals
liveness = if ($record.state -eq 'ended') { 'ended' } elseif (($Now - (ConvertTo-UtcTimestamp $record.lastSeenUtc)).TotalHours -le 2) { 'recent' } else { 'host_verification_required' }
recordRevision = [long]$record.recordRevision
}
if ([long]$record.recordRevision -gt $Cursor -and $changes.Count -lt $script:ResultLimit) { $changes.Add($summary) | Out-Null }
if ($record.state -ne 'active' -or [string]$record.idHash -eq $governorHash) { continue }
$allActive.Add($summary) | Out-Null
if ($record.kind -eq 'task' -and $activeTasks.Count -lt $script:ResultLimit) { $activeTasks.Add($summary) | Out-Null }
if ($record.kind -eq 'agent' -and $activeAgents.Count -lt $script:ResultLimit) { $activeAgents.Add($summary) | Out-Null }
}
$activeCount = $allActive.Count
$batch = [Collections.Generic.List[object]]::new()
$batchOffset = if ($activeCount -gt 0) { [int]([long]$State.health.scanOffset % $activeCount) } else { 0 }
$batchCount = [Math]::Min($script:CheckBatchLimit, $activeCount)
for ($index = 0; $index -lt $batchCount; $index++) {
$batch.Add($allActive[($batchOffset + $index) % $activeCount]) | Out-Null
}
$nextBatchOffset = if ($activeCount -gt 0) { ($batchOffset + $batchCount) % $activeCount } else { 0 }
$cycleCount = if ($null -ne $State.governor) { [long]$State.governor.cycleCount } else { 0L }
$governorAgeDays = if ($null -ne $State.governor) {
($Now - (ConvertTo-UtcTimestamp $State.governor.claimedAtUtc)).TotalDays
} else { 0 }
$rotationRequired = $cycleCount -ge $script:GovernorMaximumCycles -or $governorAgeDays -ge $script:GovernorMaximumAgeDays
$engine = if ([long]$State.health.droppedEntries -gt 0) { 'degraded' } else { 'healthy' }
return [ordered]@{
ok = $true
action = $RequestedAction
engine = $engine
schemaVersion = 2
revision = [long]$State.revision
governorTaskId = $governorId
governorClaimed = ($null -ne $State.governor)
currentIsGovernor = ($null -ne $currentHash -and $null -ne $State.governor -and $currentHash -eq [string]$State.governor.idHash)
activeTasks = $activeTasks.Count
activeAgents = $activeAgents.Count
tasks = @($activeTasks)
agents = @($activeAgents)
checkBatch = @($batch)
checkBatchLimit = $script:CheckBatchLimit
batchOffset = $batchOffset
nextBatchOffset = $nextBatchOffset
changes = @($changes)
resultTruncated = ($activeCount -gt ($activeTasks.Count + $activeAgents.Count))
registryCoverage = if ([long]$State.health.hookRuns -gt 0) { 'lifecycle_hooks_observed' } else { 'host_inventory_required' }
monitoringMode = 'complete_host_inventory_plus_optional_hooks'
monitoredTaskPolicy = 'all_live_host_tasks_including_explicit_targets'
turnSignals = [long]$State.health.turnSignals
duplicateSignals = [long]$State.health.duplicateSignals
hookModelContext = 'none'
workerModelTurns = 0
hookExecutionObservation = if ([long]$State.health.hookRuns -gt 0) { 'observed' } else { 'not_observed' }
hookTrustObservation = 'host_verification_required'
hookRole = 'optional_acceleration'
hookRequiredForAutonomy = $false
lastHookUtc = $State.health.lastHookUtc
livenessAuthority = 'complete_host_inventory'
taskDiscoveryAuthority = 'complete_host_inventory_each_governor_cycle'
catalogRefreshAction = 'fully_restart_codex_then_start_fresh_task'
loadedTaskCatalogHotSwap = 'unsupported_by_host'
taskTransport = 'host_required'
requiredHostAction = 'reconcile_host_inventory_then_wait_compact_batch'
routineUserAction = 'none'
hostEquivalenceKey = $script:HostEquivalenceKey
equivalenceScope = 'installation'
installationScopePersistence = if ($script:StateStoreMode -eq 'temp_private') { 'bounded_v3_state_root_anchor' } else { 'state_root_anchor' }
installationScopeSource = $script:InstallationScopeSource
stateStoreMode = $script:StateStoreMode
stateStoreWriteReady = $script:StateStoreWriteReady
stateStoreProtection = $script:StateStoreProtection
stateStoreMigration = $script:StateStoreMigration
stateStoreSlot = $script:StateStoreSlot
stateStoreRecovery = $script:StateStoreRecovery
stateStoreIdentity = $script:StateStoreIdentity
codexHomeSource = $script:CodexHomeSource
codexHomeIdentity = $script:CodexHomeIdentity
registryMutexIdentity = $script:RegistryMutexIdentity
hostReconcileAttemptLimit = $script:HostReconcileAttemptLimit
hostRecheckThroughCycle = $script:HostRecheckThroughCycle
hostPostcondition = 'one_live_governor_one_active_recurrence_zero_duplicates'
localMutexScope = 'machine_state_root'
workerRecurrence = 'disabled'
modelCalls = 'governor_only'
recommendedGovernorModel = 'gpt-5.6-terra'
recommendedGovernorReasoningEffort = 'medium'
recommendedCadenceMinutes = if ($activeCount -gt 0) { $script:GovernorActiveCadenceMinutes } else { $script:GovernorIdleCadenceMinutes }
maximumModelCallsPerDay = if ($activeCount -gt 0) { [int](1440 / $script:GovernorActiveCadenceMinutes) } else { [int](1440 / $script:GovernorIdleCadenceMinutes) }
governorCycleCount = $cycleCount
governorMaximumCycles = $script:GovernorMaximumCycles
governorMaximumAgeDays = $script:GovernorMaximumAgeDays
rotationRequired = $rotationRequired
registryCapacity = if ([long]$State.health.droppedEntries -gt 0) { 'exhausted' } else { 'available' }
ignoredStaleEvents = [long]$State.health.ignoredStaleEvents
}
}
function Write-SafeOutput {
param($Payload)
Write-Output ('CHRONOS SUPERVISION ' + ($Payload | ConvertTo-Json -Compress -Depth 8))
}
function Write-SafeError {
param([string]$Code, [string]$RequestedAction)
Write-SafeOutput ([ordered]@{ ok = $false; error = $Code; action = $RequestedAction })
}
$mutex = $null
$acquired = $false
$hookData = $null
$preparedHookEvent = $null
$preparedProtectedId = $null
$resolved = $null
$hookDurable = $false
$eventObservedAt = $script:InvocationObservedAtUtc
try {
if ($Action -eq 'hook') {
$reader = $null
try {
$reader = [IO.StreamReader]::new(
[Console]::OpenStandardInput(),
[Text.UTF8Encoding]::new($false, $true),
$true,
4096,
$false
)
$raw = $reader.ReadToEnd()
} catch {
throw 'supervision_hook_input_encoding_invalid'
} finally {
if ($reader) { $reader.Dispose() }
}
if ([string]::IsNullOrWhiteSpace($raw)) { throw 'supervision_hook_input_empty' }
if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
if ([Text.Encoding]::UTF8.GetByteCount($raw) -gt $script:HookInputByteLimit) { throw 'supervision_hook_input_oversize' }
Assert-StrictJson $raw 'supervision_hook_json_invalid'
try { $hookData = ConvertTo-Hashtable ($raw | ConvertFrom-Json -ErrorAction Stop) } catch { throw 'supervision_hook_json_invalid' }
if (-not ($hookData -is [Collections.IDictionary])) { throw 'supervision_hook_shape_invalid' }
if (-not [string]::IsNullOrWhiteSpace($ObservedAtUtc)) {
try { $eventObservedAt = ConvertTo-UtcTimestamp $ObservedAtUtc } catch { throw 'supervision_hook_timestamp_invalid' }
}
}
if ($Action -eq 'hook') {
$preparedHookEvent = New-PreparedHookEvent $hookData $eventObservedAt
$preparedProtectedId = if ([string]$preparedHookEvent.event -eq 'SessionStart') { [string]$preparedHookEvent.protectedSessionId } elseif ([string]$preparedHookEvent.event -eq 'SubagentStart') { [string]$preparedHookEvent.protectedAgentId } else { $null }
}
$resolved = Resolve-StatePath $StatePath
$resolved = Initialize-StateStore $resolved
$script:StateStoreIdentity = (Get-TextHash ([IO.Path]::GetFullPath($resolved).ToUpperInvariant())).Substring(0, 16)
$mutex = New-RegistryMutex $resolved
$mutexWaitMilliseconds = if ($Action -eq 'hook' -and [string](Get-Value $hookData 'hook_event_name' '') -eq 'SessionEnd') {
$script:SynchronousHookMutexWaitMilliseconds
} elseif ($Action -eq 'hook') {
$script:AsynchronousHookMutexWaitMilliseconds
} else { 1000 }
try { $acquired = $mutex.WaitOne($mutexWaitMilliseconds) } catch [Threading.AbandonedMutexException] { $acquired = $true }
if (-not $acquired) {
if ($Action -eq 'hook') {
Write-PendingHookEventDurably $resolved $preparedHookEvent
$hookDurable = $true
exit 0
}
throw 'supervision_mutex_busy'
}
Import-LegacyStateIfPresent $resolved
if ($Action -ne 'hook') {
$installationScopeId = Get-OrCreateInstallationScopeId $resolved
$script:HostEquivalenceKey = $script:GovernorEquivalencePrefix + ':' + $installationScopeId
}
$state = Read-State $resolved -ValidateProtectedIds:($Action -ne 'hook')
$now = [DateTimeOffset]::UtcNow
$processedHookEventsBeforeMerge = ConvertTo-Json -InputObject @($state.health.processedHookEvents) -Compress -Depth 4
$pendingItems = @(Merge-PendingHookEvents $state $resolved $now)
$processedHookEventsAfterMerge = ConvertTo-Json -InputObject @($state.health.processedHookEvents) -Compress -Depth 4
$processedHookEventsChanged = $processedHookEventsBeforeMerge -ne $processedHookEventsAfterMerge
if ($Action -eq 'hook') {
Invoke-HookEvent $state $hookData $now $eventObservedAt $preparedProtectedId ([string]$preparedHookEvent.workspaceHash)
Write-State $state $resolved -TrustedHookMutation
$hookDurable = $true
Remove-PendingHookEvents $pendingItems $resolved
exit 0
}
if ($pendingItems.Count -gt 0 -or $processedHookEventsChanged) {
Write-State $state $resolved -TrustedHookMutation
Remove-PendingHookEvents $pendingItems $resolved
}
if ($Action -eq 'status') {
Remove-ExpiredRecords $state $now
$governorTaskId = if ($null -ne $state.governor) { Unprotect-OpaqueId $state.governor.protectedId } else { $null }
$governorLifecycleState = if ($null -ne $state.governor) { [string]$state.sessions[[string]$state.governor.idHash].state } else { 'unclaimed' }
$activeTasks = @($state.sessions.Values | Where-Object { $_.kind -eq 'task' -and $_.state -eq 'active' -and ($null -eq $state.governor -or $_.idHash -ne $state.governor.idHash) }).Count
$activeAgents = @($state.sessions.Values | Where-Object { $_.kind -eq 'agent' -and $_.state -eq 'active' }).Count
Write-SafeOutput ([ordered]@{
ok = $true
action = 'status'
engine = if ([long]$state.health.droppedEntries -gt 0) { 'degraded' } else { 'healthy' }
schemaVersion = 2
revision = [long]$state.revision
governorClaimed = ($null -ne $state.governor)
governorTaskId = $governorTaskId
governorLifecycleState = $governorLifecycleState
governorRoleCompatibility = if ($null -ne $state.governor) { 'host_verification_required' } else { 'unclaimed' }
activeTasks = $activeTasks
activeAgents = $activeAgents
retainedRecords = $state.sessions.Count
hookRuns = [long]$state.health.hookRuns
turnSignals = [long]$state.health.turnSignals
duplicateSignals = [long]$state.health.duplicateSignals
droppedEntries = [long]$state.health.droppedEntries
ignoredStaleEvents = [long]$state.health.ignoredStaleEvents
registryCapacity = if ([long]$state.health.droppedEntries -gt 0) { 'exhausted' } else { 'available' }
registryCoverage = if ([long]$state.health.hookRuns -gt 0) { 'lifecycle_hooks_observed' } else { 'host_inventory_required' }
hookExecutionObservation = if ([long]$state.health.hookRuns -gt 0) { 'observed' } else { 'not_observed' }
hookTrustObservation = 'host_verification_required'
hookRole = 'optional_acceleration'
hookRequiredForAutonomy = $false
lastHookUtc = $state.health.lastHookUtc
livenessAuthority = 'complete_host_inventory'
taskDiscoveryAuthority = 'complete_host_inventory_each_governor_cycle'
catalogRefreshAction = 'fully_restart_codex_then_start_fresh_task'
loadedTaskCatalogHotSwap = 'unsupported_by_host'
taskTransport = 'host_required'
requiredHostAction = 'reconcile_host_inventory_then_wait_compact_batch'
routineUserAction = 'none'
hostEquivalenceKey = $script:HostEquivalenceKey
equivalenceScope = 'installation'
installationScopePersistence = if ($script:StateStoreMode -eq 'temp_private') { 'bounded_v3_state_root_anchor' } else { 'state_root_anchor' }
installationScopeSource = $script:InstallationScopeSource
stateStoreMode = $script:StateStoreMode
stateStoreWriteReady = $script:StateStoreWriteReady
stateStoreProtection = $script:StateStoreProtection
stateStoreMigration = $script:StateStoreMigration
stateStoreSlot = $script:StateStoreSlot
stateStoreRecovery = $script:StateStoreRecovery
stateStoreIdentity = $script:StateStoreIdentity
codexHomeSource = $script:CodexHomeSource
codexHomeIdentity = $script:CodexHomeIdentity
registryMutexIdentity = $script:RegistryMutexIdentity
priorStateDisposition = $script:PriorStateDisposition
priorStateWriteAttempted = $script:PriorStateWriteAttempted
hostReconcileAttemptLimit = $script:HostReconcileAttemptLimit
hostRecheckThroughCycle = $script:HostRecheckThroughCycle
hostPostcondition = 'one_live_governor_one_active_recurrence_zero_duplicates'
localMutexScope = 'machine_state_root'
workerRecurrence = 'disabled'
modelCalls = 'governor_only'
monitoringMode = 'complete_host_inventory_plus_optional_hooks'
monitoredTaskPolicy = 'all_live_host_tasks_including_explicit_targets'
hookModelContext = 'none'
workerModelTurns = 0
recurrenceEligible = ($null -ne $state.governor -and [long]$state.governor.cycleCount -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$state.governor.lastCycleUtc))
recurrenceCreationPolicy = 'after_successful_complete_host_inventory_cycle'
recommendedGovernorModel = 'gpt-5.6-terra'
recommendedGovernorReasoningEffort = 'medium'
recommendedCadenceMinutes = if (($activeTasks + $activeAgents) -gt 0) { $script:GovernorActiveCadenceMinutes } else { $script:GovernorIdleCadenceMinutes }
maximumModelCallsPerDay = if (($activeTasks + $activeAgents) -gt 0) { [int](1440 / $script:GovernorActiveCadenceMinutes) } else { [int](1440 / $script:GovernorIdleCadenceMinutes) }
governorCycleCount = if ($null -ne $state.governor) { [long]$state.governor.cycleCount } else { 0L }
governorMaximumCycles = $script:GovernorMaximumCycles
governorMaximumAgeDays = $script:GovernorMaximumAgeDays
})
exit 0
}
$current = Normalize-OpaqueId $SessionId
$currentHash = Get-TextHash $current
if ($Action -eq 'initialize') {
$sameGovernor = $null -ne $state.governor -and [string]$state.governor.idHash -eq $currentHash
if ($null -ne $state.governor -and [string]$state.governor.idHash -ne $currentHash) {
if (-not $Force) { throw 'supervision_governor_conflict' }
$priorGovernorHash = [string]$state.governor.idHash
$endedPriorGovernor = Set-RecordEndedByHash $state $priorGovernorHash $now $now
if (-not $endedPriorGovernor -or [string]$state.sessions[$priorGovernorHash].state -ne 'ended') {
throw 'supervision_event_order_conflict'
}
}
$existing = if ($state.sessions.Contains($currentHash)) { $state.sessions[$currentHash] } else { $null }
$workspaceHash = if ($null -ne $existing) { [string]$existing.workspaceHash } else { Get-WorkspaceHash (Get-Location).Path }
$model = if ($null -ne $existing) { [string]$existing.model } else { 'unavailable' }
$source = if ($null -ne $existing) { [string]$existing.source } else { 'fallback' }
$initialized = Set-SessionRecord $state $current 'task' $null $workspaceHash $model 'active' $source $now $now 3 -AllowReactivation
if (-not $initialized -or $state.sessions[$currentHash].state -ne 'active') { throw 'supervision_event_order_conflict' }
$state.revision = [long]$state.revision + 1
if ($sameGovernor) {
$state.governor.lastSeenUtc = $now.ToString('o')
} else {
$state.governor = [ordered]@{
idHash = $currentHash
protectedId = Protect-OpaqueId $current
claimedAtUtc = $now.ToString('o')
lastSeenUtc = $now.ToString('o')
cycleCount = 0L
idleCycles = 0L
lastCycleUtc = $null
}
}
Write-State $state $resolved
$initializePayload = Get-DiscoveryPayload $state 'initialize' $current $SinceRevision $now
$initializePayload['recurrenceEligible'] = $false
$initializePayload['recurrenceCreationPolicy'] = 'after_successful_complete_host_inventory_cycle'
$initializePayload['requiredHostAction'] = 'run_complete_host_inventory_cycle_before_recurrence'
Write-SafeOutput $initializePayload
exit 0
}
if ($Action -eq 'confirm-active') {
if ($null -eq $state.governor -or [string]$state.governor.idHash -ne $currentHash) { throw 'supervision_governor_mismatch' }
$subject = Normalize-OpaqueId $SubjectId 'supervision_subject_id_invalid'
$subjectHash = Get-TextHash $subject
if (-not $state.sessions.Contains($subjectHash)) { throw 'supervision_subject_unknown' }
$record = $state.sessions[$subjectHash]
$confirmed = Set-SessionRecord $state $subject ([string]$record.kind) ([string]$record.parentHash) ([string]$record.workspaceHash) ([string]$record.model) 'active' ([string]$record.source) $now $now 3 -AllowReactivation
if (-not $confirmed -or $state.sessions[$subjectHash].state -ne 'active') { throw 'supervision_event_order_conflict' }
Write-State $state $resolved
Write-SafeOutput ([ordered]@{ ok = $true; action = 'confirm-active'; revision = [long]$state.revision; subjectHash = $subjectHash.Substring(0, 16); state = 'active' })
exit 0
}
if ($Action -eq 'reconcile-host') {
if ($null -eq $state.governor -or [string]$state.governor.idHash -ne $currentHash) { throw 'supervision_governor_mismatch' }
$inventory = Complete-HostInventoryForGovernor (Read-HostInventory $HostInventoryPath $now) $current
$reconciled = Invoke-HostInventoryReconciliation $state $inventory $current $now
$state.governor.lastSeenUtc = $now.ToString('o')
$payload = Get-DiscoveryPayload $state 'reconcile-host' $current $SinceRevision $now
$payload['hostInventoryObserved'] = [int]$reconciled.Observed
$payload['hostInventoryRawObserved'] = [int]$inventory.RawObserved
$payload['hostInventoryComplete'] = [bool]$reconciled.Complete
$payload['hostInventoryCallerVisibility'] = [string]$inventory.CallerVisibility
$payload['hostInventoryGovernorSource'] = [string]$inventory.GovernorSource
$payload['hostTasksAdded'] = [int]$reconciled.Added
$payload['hostTasksReactivated'] = [int]$reconciled.Reactivated
$payload['hostTaskGenerationsChanged'] = [int]$reconciled.GenerationChanged
$payload['hostTasksEnded'] = [int]$reconciled.Ended
$payload['hostTaskStatuses'] = @(Get-CompactHostTaskStatuses $inventory)
$payload['taskWakePolicy'] = 'intervention_claim_required'
$payload['requiredHostAction'] = 'wait_compact_batch_then_evaluate_heartbeat'
$payload['recurrenceEligible'] = $false
$payload['recurrenceCreationPolicy'] = 'after_successful_complete_host_inventory_cycle'
Write-State $state $resolved
Write-SafeOutput $payload
exit 0
}
if ($Action -eq 'release') {
if ($null -eq $state.governor -or [string]$state.governor.idHash -ne $currentHash) { throw 'supervision_governor_mismatch' }
if (-not $ConfirmRecurrenceStopped) {
Write-SafeOutput ([ordered]@{
ok = $true
action = 'release'
releaseReady = $false
governorClaimed = $true
requiredHostAction = 'pause_or_delete_chronos_governor_pulse_then_verify_absent'
})
exit 0
}
$state.revision = [long]$state.revision + 1
$state.governor = $null
Write-State $state $resolved
Write-SafeOutput ([ordered]@{ ok = $true; action = 'release'; revision = [long]$state.revision; governorClaimed = $false })
exit 0
}
if ($Action -eq 'cycle') {
if ($null -eq $state.governor -or [string]$state.governor.idHash -ne $currentHash) { throw 'supervision_governor_mismatch' }
$inventory = Read-HostInventory $HostInventoryPath $now
if (-not [bool]$inventory.Complete) { throw 'supervision_host_inventory_incomplete' }
$inventory = Complete-HostInventoryForGovernor $inventory $current
$reconciled = Invoke-HostInventoryReconciliation $state $inventory $current $now
$state.governor.cycleCount = [long]$state.governor.cycleCount + 1
$state.governor.lastCycleUtc = $now.ToString('o')
$state.governor.lastSeenUtc = $now.ToString('o')
$activeCount = @($state.sessions.Values | Where-Object { $_.state -eq 'active' -and $_.idHash -ne $state.governor.idHash }).Count
$state.governor.idleCycles = if ($activeCount -gt 0) { 0L } else { [long]$state.governor.idleCycles + 1 }
$payload = Get-DiscoveryPayload $state 'cycle' $current $SinceRevision $now
$payload['hostInventoryObserved'] = [int]$reconciled.Observed
$payload['hostInventoryRawObserved'] = [int]$inventory.RawObserved
$payload['hostInventoryComplete'] = $true
$payload['hostInventoryCallerVisibility'] = [string]$inventory.CallerVisibility
$payload['hostInventoryGovernorSource'] = [string]$inventory.GovernorSource
$payload['hostInventoryCycle'] = [long]$state.governor.cycleCount
$payload['hostTasksAdded'] = [int]$reconciled.Added
$payload['hostTasksReactivated'] = [int]$reconciled.Reactivated
$payload['hostTaskGenerationsChanged'] = [int]$reconciled.GenerationChanged
$payload['hostTasksEnded'] = [int]$reconciled.Ended
$payload['hostTaskStatuses'] = @(Get-CompactHostTaskStatuses $inventory)
$payload['taskWakePolicy'] = 'intervention_claim_required'
$payload['requiredHostAction'] = 'wait_compact_batch_then_evaluate_heartbeat'
$payload['recurrenceEligible'] = $true
$payload['recurrenceCreationPolicy'] = 'after_successful_complete_host_inventory_cycle'
$state.health.scanOffset = [long]$payload.nextBatchOffset
Write-State $state $resolved
Write-SafeOutput $payload
exit 0
}
if ($Action -eq 'discover') {
if ($null -eq $state.governor -or [string]$state.governor.idHash -ne $currentHash) { throw 'supervision_governor_mismatch' }
$payload = Get-DiscoveryPayload $state 'discover' $current $SinceRevision $now
$payload['cycleAdvanced'] = $false
$payload['taskWakePolicy'] = 'intervention_claim_required'
$payload['requiredHostAction'] = 'run_cycle_with_complete_host_inventory'
Write-SafeOutput $payload
exit 0
}
throw 'supervision_action_invalid'
} catch {
$code = [string]$_.Exception.Message
if ($code -notmatch '^supervision_[a-z_]+$') { $code = 'supervision_internal_error' }
if ($Action -eq 'hook') {
# A lifecycle hint must reach either the registry or the protected pending
# queue before the production hook returns silently. This also catches a
# transient direct-state or first pending-slot failure without retrying the
# model task or extending the mutex deadline.
if (-not $hookDurable -and $null -ne $preparedHookEvent -and -not [string]::IsNullOrWhiteSpace([string]$resolved)) {
try {
Write-PendingHookEventDurably $resolved $preparedHookEvent
$hookDurable = $true
exit 0
} catch {
$fallbackCode = [string]$_.Exception.Message
if ($fallbackCode -match '^supervision_[a-z_]+$') { $code = $fallbackCode }
else { $code = 'supervision_pending_event_unwritable' }
}
}
if ($Diagnostic) {
Write-SafeError $code 'hook'
exit 1
}
exit 0
}
Write-SafeError $code $Action
exit 1
} finally {
if ($mutex) {
if ($acquired) { try { $mutex.ReleaseMutex() | Out-Null } catch {} }
$mutex.Dispose()
}
}
SHA-256: b7cd29439f1d88b0684002df1f3cb7f4aed4de0d59c3e53effa61fc8fb3fda91