Assets/Resolve-InstanceConversation.ps1
|
# SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 Tyler Vigario <# Which conversation an instance launches with. Dot-sourced by greenroom-launch.ps1 rather than imported from the module. The launcher finds this file next to itself via $PSScriptRoot, the same way the .vbs finds the watchdog and the watchdog finds the launcher, so the whole chain relocates together. `Import-Module Greenroom` would have been the obvious alternative and is wrong here: the task hard-codes a VERSIONED asset path, so an import could resolve to a different version than the assets being run -- the exact drift Get-InstanceAssetVersion exists to catch. Kept free of side effects on purpose. It reads, it decides, it returns; the caller does the writing. That is what makes it testable without a running instance, and the asset layer had no behavioural coverage at all before it. #> function Resolve-InstanceConversation { <# .SYNOPSIS Decide which conversation to launch with, and whether to record the choice. .OUTPUTS SessionId - the id to pass to claude Action - 'resume' (--resume, conversation exists) or 'create' (--session-id) Persist - whether the caller should write SessionId to StatePath Reason - one line for the log, explaining which branch was taken #> [CmdletBinding()] param( # The project store for this instance's working directory. [Parameter(Mandatory)][string]$Store, # conversation.json for this instance. [Parameter(Mandatory)][string]$StatePath ) # A transcript is named for its session id. Anything else in the store is not a # conversation and must never reach --resume: a bogus id exits 1, and the launcher # runs in a hidden window, so the watchdog would crash-loop with nothing on screen. $uuid = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' $pinned = $null if (Test-Path -LiteralPath $StatePath) { try { $pinned = (Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json).sessionId } catch { $pinned = $null } # unreadable state reads as no state, never fatal } # The pin gets the SAME rule as the store listing. conversation.json is a file on # disk and is hand-written during a deliberate cutover, so it is untrusted input: a # value that is not a session id must never reach --resume. A bare existence check is # not enough, because the way a malformed pin survives one is by naming a file that # happens to exist. A malformed pin reads as no pin, and says so in the log rather # than being dropped quietly -- the window is hidden, so silence is the failure mode. $note = '' if ($null -ne $pinned -and ($pinned -isnot [string] -or $pinned -notmatch $uuid)) { $note = "ignoring malformed pin '$pinned' -- " $pinned = $null } if ($pinned) { if (Test-Path -LiteralPath (Join-Path $Store "$pinned.jsonl")) { return [pscustomobject]@{ SessionId = $pinned Action = 'resume' Persist = $false Reason = "resuming pinned conversation $pinned" } } # Pinned but gone -- store cleared, instance relocated, transcript deleted. Mint # rather than resume something that is not there. $fresh = [guid]::NewGuid().ToString() return [pscustomobject]@{ SessionId = $fresh Action = 'create' Persist = $true Reason = "pinned conversation $pinned has no transcript -- pinning new $fresh" } } # No pin yet. Adopt the newest conversation already in this working directory and pin # it from here on. # # This is BOOTSTRAP, not migration. It is not here to carry old installs across a # version boundary -- it is how an instance installed into a directory that already # holds conversations picks one up, which is as true of a fresh install in two years # as it is of an upgrade today. Recency is used exactly once, to choose a starting # point; identity governs every launch after it. $existing = @(Get-ChildItem -LiteralPath $Store -Filter *.jsonl -ErrorAction SilentlyContinue | Where-Object { $_.BaseName -match $uuid } | Sort-Object LastWriteTime) if ($existing.Count -gt 0) { $adopt = $existing[-1].BaseName return [pscustomobject]@{ SessionId = $adopt Action = 'resume' Persist = $true Reason = "$note" + "adopting newest of $($existing.Count) existing conversation(s) and pinning it: $adopt" } } $fresh = [guid]::NewGuid().ToString() return [pscustomobject]@{ SessionId = $fresh Action = 'create' Persist = $true Reason = "$note" + "no existing conversation -- pinning new $fresh" } } |