fix(deploy): make orphaned Kit visible to liveness and fail Phase 4c closed (#640) - #653
Merged
Merged
Conversation
…closed (#640) Liveness was tracked as the LAUNCHER's pid, but the process holding the streaming ports, the GPU context and the Omniverse user directory is its Kit child. When the launcher died and the child survived, Remove-StalePidFile correctly deleted the pid file, Test-AlreadyRunning correctly reported "not running", and Phase 4c started a second Kit into the live one - which deadlocked in early startup with two futex-waiting threads, no listener and not one line of its own log, and was only noticed 480s later. Two halves: - Start-HostNativeService now records the TCP ports a launch claims in a <Name>.ports sidecar, written BEFORE the launch and deliberately left in place by Remove-StalePidFile. That record is the surviving trace of an orphaned child; only a stop that actually terminated something clears it. Start-HostNativeKit declares its signal + spectator signal ports. - Get-HostNativeOrphanListener answers "is a process outside our recorded tree still holding these ports?" over the union of the recorded claim and this run's expected ports, and Phase 4c consults it immediately before launching. An unaccounted holder is a hard stop naming the ports, the pids and scripts/stop-all.ps1 - the manual recovery that made the failing deployment pass unchanged - instead of a second instance. A holder that vanishes within a bounded settle window is teardown, not an orphan. Phase 1 already printed this exact observation ("occupied by our PID ... already running, will skip start"); it was never a gate. Now it is. Fixes #640. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSyeoncjEm8rcfRDgnS6CB
…ntry (#640) scripts/deploy.ps1 and scripts/lib/host-native-launcher.ps1 are both classified verification-mechanism paths, and this PR changes what "already running / safe to start" means on the canonical deploy path. Per docs/agents/self-referential-bootstrap.md §2.1 class 2, the deploy contract only ever rebuilds and verifies already-merged origin/main content, so the changed behaviour - a port claim written by a live launch, and a Phase 4c refusal that only fires against a real orphaned Kit holding a real LISTEN socket - cannot be proven by the canonical mechanism before this change reaches origin/main. Same shape as PR #647. Fixture-driven unit tests prove the detection and refusal logic; they do not replace a canonical deployment cycle, which is exactly what the fixpoint closure will re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSyeoncjEm8rcfRDgnS6CB
monkey1sai-blip
approved these changes
Aug 19, 2026
monkey1sai-blip
left a comment
Collaborator
There was a problem hiding this comment.
{"kind":"ai-bim-automated-approve-only","version":1,"automated":true,"repo":"monkey1sai/AI-BIM-governance","prNumber":653,"headOid":"0724397deb2861b06e5280a3aefb1ef8347e68ee","baseOid":"4c521d911ac936c6d6da9613682e28fbc76ab327","action":"approve-only"}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Liveness for the host-native Kit was tracked as the launcher's pid, but the
process holding the streaming ports, the GPU context and the Omniverse user
directory is its Kit child. On the canonical Linux target the launcher died
while the Kit child survived, and every link in the chain then behaved exactly
as written:
Remove-StalePidFilecorrectly deletedbim-streaming-server.pid- the pidit recorded really was gone.
Test-AlreadyRunningcorrectly reported "not running" - it can only see thatpid file.
deadlocked in early startup:
S (sleeping), two threads, both infutex_wait_queue, no listener, and not one line in its own Omniverse log.The deploy found out 480s later (
deploy_exit_code = 4).Phase 1 had already printed the orphan
(
[skip ] port TCP/49150 occupied by our PID 188705 (kit.exe) - already running, will skip start).That observation never reached the start decision; the "will skip start" was an
unkept promise. This PR closes both halves.
1. Record the real resource holder.
Start-HostNativeServicegains-ListenPortsand writes a<Name>.portssidecar - the same idiom as theexisting
<Name>.jobsidecar. It is written before the launch (it is aclaim, not an observation: a child may bind before we could record anything
afterwards), and
Remove-StalePidFiledeliberately leaves it in place. Thatrecord is the only surviving trace of an orphaned child once the pid file is
gone. Only a
Stop-HostNativeServicethat actually terminated something clearsit; a stop that stopped nothing keeps it.
Start-HostNativeKitdeclares itssignal + spectator signal ports (TCP - the UDP media ports stay out of a record
that only a TCP probe can attribute).
2. Fail the preflight closed.
Get-HostNativeOrphanListeneranswers "is aprocess outside our recorded tree still holding these ports?" over the union
of the recorded claim and the ports this run expects, so neither a config change
nor a first-ever launch can hide a holder. "Accounted for" is narrow on purpose:
the pid file must exist, its process must be alive, and the listener must be
that process or one of its descendants. No pid file, a dead recorded pid, an
unrelated holder, or an owner the OS will not reveal
(
Get-PlatformTcpListenerPidreturns-1) are all unaccounted. Phase 4cconsults it immediately before
Start-HostNativeKitand refuses withexit 4,naming the ports, the pids and
scripts/stop-all.ps1- which stops by port aswell as by pid file, so it reaches an orphan whose pid file is already gone.
That is exactly the manual recovery that made the failing deployment pass
unchanged.
A holder that disappears within a bounded settle window (3s, polled) is
teardown, not an orphan: Phase 4c also reaches this gate immediately after
stopping the previous tree itself when runtime parameters changed, and a
force-killed process does not release its listening socket at the instant the
stop call returns.
Cross-platform: the detector uses only
Get-PlatformTcpListenerPidandGet-PlatformChildProcessIds, which the platform adapter already implements forboth Windows (Get-NetTCPConnection / CIM) and Linux (
ss -ltnpH//proc). Nonew platform primitive, and no WMI-only parent walking.
Fixes #640.
AI Coding Governance
scripts/**is human_critical and the counted approval must come from the fixed owner accountgitnexus impact Get-HostNativeOrphanListener -d upstream -r AI-BIM-governancereturnsTarget not foundbecause GitNexus does not extract PowerShell function-level symbols (.ps1files are indexed as File nodes only) - the same coverage gap recorded for PR #647, treated as unavailable and not as a pass. Substitute evidence: manual call-site inventory -Get-HostNativeOrphanListenerhas exactly two consumers (scripts/deploy.ps1Phase 4c andscripts/tests/test-host-native-launcher.ps1), and every reader/writer of the new<Name>.portssidecar is enumerated (Start-HostNativeService,Stop-HostNativeService,Get-HostNativeServiceListenPorts, andRemove-StalePidFilewhich deliberately does not touch it)Deploy Path Verification
scripts/.run/<name>.portssidecar). No port number, Docker, viewer, or env-file changescripts/deploy.ps1Phase 4c now evaluatesGet-HostNativeOrphanListenerbeforeStart-HostNativeKitand exits 4 on an unaccounted holder instead of launching a second Kitpwsh -NoProfile -NonInteractive -File scripts/deploy.ps1 -DryRun-> exit 0 in 6s on Windows at exact headeba8ca81c05e2f42b34654ffb3d95a1e338e96ad, run detached with stdout/stderr redirected to files (stderr 0 bytes); markers[skip ] Phase 2 auto-fix DRY-RUN (no actions executed)andStorage root: ...\storage (ALIGNED)pwsh -NoProfile -NonInteractive -File scripts/tests/test-host-native-launcher.ps1andpwsh -NoProfile -NonInteractive -File scripts/tests/test-deploy-governance-static.ps1scripts/tests/test-host-native-launcher.ps1(five new executable #640 cases),scripts/tests/test-deploy-governance-static.ps1(Phase 4c wiring and ordering),docs/evidence/orphan-kit-liveness-preflight/self-referential-bootstrap/Windows On-Demand Verification
eba8ca81c05e2f42b34654ffb3d95a1e338e96ad; exact-head Windows execution: therebuild/test-deploy contractsjob runs on Windows Server 2025 and executedscripts/tests/test-host-native-launcher.ps1including all five new#640cases, every one[PASS], ending=== test-host-native-launcher.ps1: ALL PASSED ===- run https://github.com/monkey1sai/AI-BIM-governance/actions/runs/32244348942/job/96041843520 (CI run 32244348942 concluded success at this head). Locally on Windows at the same head:pwsh -NoProfile -NonInteractive -File scripts/deploy.ps1 -DryRun-> exit 0 in 6s, run detached with stdout/stderr redirected to files, stderr 0 bytes, markers[skip ] Phase 2 auto-fix DRY-RUN (no actions executed)andStorage root: ...\storage (ALIGNED). No live deployment, no service start, no port bind, and no Kit / GPU / WebRTC / browser evidence is claimedSelf-Referential Bootstrap
scripts/deploy.ps1andscripts/lib/host-native-launcher.ps1are bothclassified verification-mechanism paths, and this PR changes what "already
running / safe to start" means on the canonical deploy path.
<Name>.portsclaim is written by a liveStart-HostNativeService, and the Phase 4c refusal only fires against a real orphaned Kit child still holding a real LISTEN socket after its launcher died. Neither state can be produced on a branch by the canonical mechanism before this change reaches origin/main; the unit suite proves the detection and refusal logic with injected port, process and child-enumeration probes and with sidecar fixtures, but a fixture orphan is not a canonical deployment cycle.Determination made against
docs/agents/self-referential-bootstrap.md, notagainst convenience. §2.1 class 2 ("canonical deploy path - 部署契約只重建/驗證
已 merge 的內容,branch 上取不到 post-change canonical evidence") applies
directly: both changed mechanism paths sit on that path, and §1's defining
feature - "契約禁止在 merge 前用正規機制對變更後行為取證" - is met, because the
only way to exercise the new gate through the canonical mechanism is a real
deployment against
origin/main. This is not the §2.1 exclusion ("新腳本自行定義自己的報告格式"); nothing here is a telemetry report format. The base
ledger had zero open entries, so the §3 debt gate does not block opening one.
Precedent is unambiguous: every substantive
scripts/deploy.ps1behaviourchange in the ledger era (#467, #484, #513, #573) opened an entry, and #647
opened one for the same "only a live canonical deployment can prove this" shape.
Bootstrap evidence:
docs/evidence/orphan-kit-liveness-preflight/self-referential-bootstrap/.Fixpoint (mainline re-run of the same five-command contract after merge) will
land in a dedicated, ledger-only closure PR per §3 "Closure 單一目的" - the same
treatment #652 gave #647's entry.
Validation
Every command below ran on Windows in this branch's worktree at exact head
eba8ca81c05e2f42b34654ffb3d95a1e338e96ad.pwsh -NoProfile -NonInteractive -ExecutionPolicy Bypass -File scripts/tests/test-host-native-launcher.ps1->ALL PASSED, exit 0, including the five new#640cases.pwsh -NoProfile -NonInteractive -File scripts/tests/test-deploy-governance-static.ps1->PASS deploy governance static checks, exit 0, including the new Phase 4c assertions.pwsh -NoProfile -NonInteractive -File scripts/tests/test-self-referential-bootstrap.ps1->all assertions passed, exit 0 (ledger integrity plus command-id resolvability with the new entry).pwsh -NoProfile -NonInteractive -File scripts/tests/test-pr-body-evidence.ps1-> exit 0.pwsh -NoProfile -NonInteractive -File scripts/tests/invoke-powershell-static.ps1->[invoke-powershell-static] passed, exit 0.pwsh -NoProfile -NonInteractive -File scripts/tests/test-agent-governance-check.ps1-># tests 46 / # pass 46 / # fail 0, exit 0 (no pinned literal in that suite covers the changed scripts, so nothing needed syncing).pwsh -NoProfile -NonInteractive -File scripts/tests/test-rebuild-test-deploy.ps1->[PASS] rebuild-test-deploy, exit 0.pwsh -NoProfile -NonInteractive -File scripts/tests/test-preflight-ports.ps1->ALL PASSED, exit 0.pwsh -NoProfile -NonInteractive -File scripts/deploy.ps1 -DryRun-> exit 0 in 6s, detached, stderr 0 bytes, no deploy-target mutation.git diff --cached --check-> clean.scripts/deploy.ps1andscripts/lib/host-native-launcher.ps1.New executable coverage, all fixture- and injection-driven (no Kit launched, no
real port bound; the only real processes started are short-lived Python sleeps
that bind nothing):
Remove-StalePidFileremoves the dead pid file and keeps the port claim - the exact record deploy: 孤兒 Kit 對 Test-AlreadyRunning 隱形,Phase 4c 會啟動第二個實例並死鎖 #640 lost.-1owner-not-visible -> reported (fails closed, never reads as free); every port free -> not reported; recorded and expected ports are both probed; nothing recorded and nothing expected -> no probe at all.scripts/deploy.ps1statically pinned: the gate is evaluated beforeStart-HostNativeKit, covers$resolvedKitSignalPortplus$resolvedSpectatorSignalPorts, refuses withstage=4c, namesscripts/stop-all.ps1, and exits throughPrint-FinalSummary -ExitCode 4.Known Risks and Gaps
scripts/stop-all.ps1before re-running. The refusal message says so explicitly.Stop-HostNativeProcessTreeAndWait. This PR refuses instead: the detector cannot distinguish "our orphan" from "a stranger on our port" with certainty, and killing an unidentified process from the deploy path is a destructive action that belongs behind explicit operator intent.scripts/stop-all.ps1already performs exactly that cleanup under the operator's hand, and its own ownership check (Test-IsWorkspaceProcess) is stricter than anything this gate could assert.Start-HostNativeConversionhas the same wrapper/child shape and would benefit from the same record, but issue conversion: 服務把 STORAGE_ROOT 預設成不可能正確的路徑並靜默建目錄,start-all 起的機器會拒收每個 IFC #626 landed in that start path today and the scope here is deliberately surgical; the generic-ListenPortsmechanism is ready for it.governance-serviceandkit-manager-apido not have the gap at all - their recorded pid is the uvicorn listener, with no wrapper/child split.Get-PlatformTcpListenerPidis what can attribute an owner on both platforms. An orphan holding only UDP media ports would not be detected; in the observed failure the orphan held a TCP signal port.Get-PlatformProcessIdentitybirth tokens; that is a heavier contract than a start-path gate needs, and the failure direction here is one extra refusal rather than one wrongly permitted second instance.impact/detect-changesare unavailable for PowerShell symbols (see the governance table); recorded as UNKNOWN, not as a pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01GSyeoncjEm8rcfRDgnS6CB