@trillboards/edge-sdk 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/macos/scripts/setup-litertlm.sh +35 -10
- package/deploy/windows/README.md +17 -4
- package/deploy/windows/e2e/test-headless-principal.ps1 +632 -0
- package/deploy/windows/ensure-sensing.ps1 +91 -1
- package/deploy/windows/register-litertlm-task.ps1 +114 -15
- package/deploy/windows/register-task.ps1 +162 -12
- package/deploy/windows/setup-litertlm.ps1 +78 -32
- package/dist/CloudSensingManager.d.ts +13 -4
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +6 -0
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/LiteRtVlmProvider.d.ts +108 -45
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js +124 -84
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localSenseE2eHarness.d.ts +17 -4
- package/dist/localSenseE2eHarness.d.ts.map +1 -1
- package/dist/localSenseE2eHarness.js +11 -1
- package/dist/localSenseE2eHarness.js.map +1 -1
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +49 -31
- package/dist/sensingRunMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,7 +40,97 @@ if (-not $task) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
# MIGRATION. Installs made before the headless fix carry a logon-only task, so
|
|
44
|
+
# they still go dark after an unattended reboot even though the shipped scripts
|
|
45
|
+
# on disk are now correct. There is no auto-updater, so the installer is not the
|
|
46
|
+
# only path that has to repair this. Re-register when -- and only when -- the
|
|
47
|
+
# registered task has NO logon-free start path: after one repair this is a
|
|
48
|
+
# no-op, so it cannot churn a healthy install or interrupt sensing on every
|
|
49
|
+
# wizard close.
|
|
50
|
+
#
|
|
51
|
+
# Both shapes count as repaired. register-task.ps1 registers a boot trigger only
|
|
52
|
+
# when it can (Microsoft allows those to Administrators only) and otherwise
|
|
53
|
+
# lands on a repeating trigger, which is the shape a normal no-admin install
|
|
54
|
+
# gets. Keying this check on <BootTrigger> alone would re-register that install
|
|
55
|
+
# on EVERY wizard close.
|
|
56
|
+
$needsHeadlessRepair = $false
|
|
57
|
+
try {
|
|
58
|
+
$definition = Export-ScheduledTask -TaskName $TaskName
|
|
59
|
+
$needsHeadlessRepair = ($definition -notmatch "<BootTrigger>") -and ($definition -notmatch "<Repetition>")
|
|
60
|
+
} catch {
|
|
61
|
+
Write-Host "Could not read the current task definition: $($_.Exception.Message)"
|
|
62
|
+
}
|
|
63
|
+
if ($needsHeadlessRepair) {
|
|
64
|
+
$register = Join-Path $InstallDir "bin\register-task.ps1"
|
|
65
|
+
if (Test-Path $register) {
|
|
66
|
+
Write-Host "Upgrading '$TaskName' from a logon-only registration to a startup one."
|
|
67
|
+
# Non-fatal: the final runnable check below is the loud signal. A failed
|
|
68
|
+
# repair must not be what stops a customer closing the setup window.
|
|
69
|
+
try { & $register -InstallDir $InstallDir | Out-Null }
|
|
70
|
+
catch { Write-Host "Headless re-registration failed: $($_.Exception.Message)" }
|
|
71
|
+
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# THE ONE UAC PROMPT. Everything above works unelevated, but the PRINCIPAL does
|
|
76
|
+
# not: measured on windows-latest (run 31836760844), a standard user registering
|
|
77
|
+
# for ITSELF is refused S4U with "Access is denied", because S4U performs a batch
|
|
78
|
+
# logon and Task Scheduler only grants SeBatchLogonRight when the CALLER is an
|
|
79
|
+
# administrator. An unelevated install therefore lands on 'Interactive + repeat',
|
|
80
|
+
# which still needs somebody logged on -- i.e. the reboot defect is NOT fixed.
|
|
81
|
+
# One elevation is the entire difference, so ask for it HERE: this runs at setup
|
|
82
|
+
# -wizard close, with the customer present, and register-task.ps1 repairs the
|
|
83
|
+
# model-server task in the same elevated pass so this is the only prompt.
|
|
84
|
+
#
|
|
85
|
+
# Every guard below exists to make sure we never prompt when it cannot help:
|
|
86
|
+
# - already headless -> nothing to gain
|
|
87
|
+
# - not UserInteractive -> no desktop to show a dialog on; -Verb RunAs
|
|
88
|
+
# would throw or hang an unattended install
|
|
89
|
+
# - not admin-capable -> UAC would show an over-the-shoulder CREDENTIAL
|
|
90
|
+
# prompt to a standard user who cannot satisfy it
|
|
91
|
+
# - already elevated -> register-task.ps1 above already used it
|
|
92
|
+
# A declined or failed prompt is NOT an error: the unelevated registration from
|
|
93
|
+
# the ladder is still in place and still better than what shipped before.
|
|
94
|
+
function Test-IsElevated {
|
|
95
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
96
|
+
(New-Object System.Security.Principal.WindowsPrincipal($identity)).IsInRole(
|
|
97
|
+
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
98
|
+
}
|
|
99
|
+
function Test-AdminCapable {
|
|
100
|
+
# On a filtered admin token the Administrators SID is still present (deny-only),
|
|
101
|
+
# which is exactly the population for whom one UAC click changes the outcome.
|
|
102
|
+
foreach ($group in [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups) {
|
|
103
|
+
if ($group.Value -eq 'S-1-5-32-544') { return $true }
|
|
104
|
+
}
|
|
105
|
+
return $false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$isHeadless = $false
|
|
109
|
+
try {
|
|
110
|
+
$current = Export-ScheduledTask -TaskName $TaskName
|
|
111
|
+
$isHeadless = (($current -match "<BootTrigger>") -or ($current -match "<Repetition>")) -and
|
|
112
|
+
($current -notmatch "<LogonType>InteractiveToken</LogonType>")
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
if ((-not $isHeadless) -and [Environment]::UserInteractive -and (Test-AdminCapable) -and -not (Test-IsElevated)) {
|
|
116
|
+
$register = Join-Path $InstallDir "bin\register-task.ps1"
|
|
117
|
+
if (Test-Path $register) {
|
|
118
|
+
Write-Host "Asking for one elevation so sensing can resume after an unattended restart."
|
|
119
|
+
try {
|
|
120
|
+
$psExe = Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
121
|
+
$psArgs = @(
|
|
122
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden',
|
|
123
|
+
'-File', ('"' + $register + '"'), '-InstallDir', ('"' + $InstallDir + '"')
|
|
124
|
+
)
|
|
125
|
+
Start-Process -FilePath $psExe -ArgumentList $psArgs -Verb RunAs -WindowStyle Hidden -Wait -ErrorAction Stop
|
|
126
|
+
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
127
|
+
} catch {
|
|
128
|
+
Write-Host "Elevated re-registration was not completed ($($_.Exception.Message)). Keeping the unelevated registration."
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ($task -and $task.State -eq 'Disabled') {
|
|
44
134
|
Enable-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null
|
|
45
135
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
46
136
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
<#
|
|
2
|
-
register-litertlm-task.ps1
|
|
3
|
-
LiteRT-LM on-device VLM server (Gemma 4 E2B)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
register-litertlm-task.ps1 -- register the Scheduled Task that runs the
|
|
3
|
+
LiteRT-LM on-device VLM server (Gemma 4 E2B) with crash-restart. It runs the
|
|
4
|
+
SAME degradation ladder as register-task.ps1 and must land on the same rung --
|
|
5
|
+
this task is not optional furniture, because the connector's default
|
|
6
|
+
auto-sense mode WAITS on the OpenAI-compatible endpoint at 127.0.0.1:<Port>,
|
|
7
|
+
so a connector that comes back headless while this one still waits for a logon
|
|
8
|
+
is a connector that runs and senses nothing.
|
|
9
|
+
|
|
10
|
+
This script never prompts. Headless costs one elevation on Windows (S4U needs
|
|
11
|
+
the account to hold SeBatchLogonRight, which Task Scheduler grants only to an
|
|
12
|
+
administrator caller), and a second UAC dialog in one install is not
|
|
13
|
+
acceptable -- so ensure-sensing.ps1 owns the single prompt and register-task.ps1
|
|
14
|
+
repairs THIS task inside that same elevated pass. Unelevated, both land on
|
|
15
|
+
'Interactive + repeat' together, which is consistent rather than headless.
|
|
16
|
+
Mirrors the macOS com.trillboards.litertlm LaunchAgent (KeepAlive=true +
|
|
17
|
+
ThrottleInterval=30). Invoked by setup-litertlm.ps1.
|
|
8
18
|
#>
|
|
9
19
|
param(
|
|
10
20
|
[Parameter(Mandatory = $true)][string]$LitertLmPath,
|
|
@@ -14,29 +24,118 @@ $ErrorActionPreference = "Stop"
|
|
|
14
24
|
|
|
15
25
|
$TaskName = "TrillboardsLiteRtLm"
|
|
16
26
|
$UserId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
17
|
-
# litert-lm serve --host 127.0.0.1 --port <port> (loopback only
|
|
27
|
+
# litert-lm serve --host 127.0.0.1 --port <port> (loopback only -- the connector
|
|
18
28
|
# is local; never expose the model server off-box).
|
|
19
29
|
$argument = 'serve --host 127.0.0.1 --port ' + $Port
|
|
20
30
|
$Action = New-ScheduledTaskAction -Execute $LitertLmPath -Argument $argument
|
|
21
|
-
|
|
31
|
+
|
|
32
|
+
# NOT AT LOGON -- see register-task.ps1 for the full rationale, including why
|
|
33
|
+
# the REPEATING trigger (not the boot trigger) is the load-bearing one: only an
|
|
34
|
+
# Administrators-group member can create a task with a boot trigger, and this
|
|
35
|
+
# runs from a no-admin installer.
|
|
36
|
+
$RepeatTrigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(1)) `
|
|
37
|
+
-RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650)
|
|
38
|
+
# The boot delay is longer here than the connector's 30s because this process
|
|
39
|
+
# memory-maps a ~2.4 GB model off a disk that is still busy with the rest of boot.
|
|
40
|
+
$BootTrigger = New-ScheduledTaskTrigger -AtStartup
|
|
41
|
+
# Best-effort, exactly as in register-task.ps1: never fail registration over it.
|
|
42
|
+
try { $BootTrigger.Delay = "PT1M" } catch { Write-Warning "Could not set the boot-trigger delay: $($_.Exception.Message)" }
|
|
43
|
+
# Fastest re-arm when someone does log on.
|
|
44
|
+
$LogonTrigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId
|
|
45
|
+
|
|
22
46
|
# RestartCount/Interval == the plist's ThrottleInterval crash-loop guard (e.g.
|
|
23
47
|
# while the ~2.4 GB model is still importing on first run); Zero time limit ==
|
|
24
|
-
# the always-on KeepAlive serve endpoint.
|
|
48
|
+
# the always-on KeepAlive serve endpoint. MultipleInstances IgnoreNew matters
|
|
49
|
+
# now that several triggers exist: a second `serve` would fail to bind $Port anyway.
|
|
25
50
|
$Settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
|
|
26
51
|
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
|
|
52
|
+
-MultipleInstances IgnoreNew `
|
|
27
53
|
-ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
28
|
-
$Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited
|
|
29
54
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
# S4U, NOT SystemAccount: litert-lm.exe itself lives under the user profile
|
|
56
|
+
# ($env:USERPROFILE\.local\bin, setup-litertlm.ps1:23) and loads the model from
|
|
57
|
+
# $env:USERPROFILE\.litert-lm (:25). SYSTEM would resolve both to
|
|
58
|
+
# C:\Windows\system32\config\systemprofile and the server would not start at all.
|
|
59
|
+
function New-LitertPrincipal([string]$LogonType) {
|
|
60
|
+
New-ScheduledTaskPrincipal -UserId $UserId -LogonType $LogonType -RunLevel Limited
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$Description = "Trillboards LiteRT-LM - on-device Gemma VLM server (:$Port)"
|
|
64
|
+
|
|
65
|
+
# Same degradation ladder as register-task.ps1, for the same reasons.
|
|
66
|
+
$Attempts = @(
|
|
67
|
+
@{ Label = "S4U + boot + repeat"; LogonType = "S4U"; Triggers = @($BootTrigger, $RepeatTrigger, $LogonTrigger) },
|
|
68
|
+
@{ Label = "S4U + repeat"; LogonType = "S4U"; Triggers = @($RepeatTrigger, $LogonTrigger) },
|
|
69
|
+
@{ Label = "Interactive + repeat"; LogonType = "Interactive"; Triggers = @($RepeatTrigger, $LogonTrigger) },
|
|
70
|
+
@{ Label = "Interactive + logon only"; LogonType = "Interactive"; Triggers = @($LogonTrigger) }
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# STOP THE RUNNING SERVE BEFORE RE-REGISTERING, and wait for it to actually go.
|
|
74
|
+
#
|
|
75
|
+
# Unregistering a task is not documented to terminate a running instance, and
|
|
76
|
+
# `litert-lm serve` is a long-lived process holding :$Port. Left alive it keeps
|
|
77
|
+
# ANSWERING - so a version upgrade would install the new CLI, re-register the
|
|
78
|
+
# task, fail to bind the port, and leave the OLD binary serving while every
|
|
79
|
+
# health probe passed. That is precisely how a host could sit on a litert-lm
|
|
80
|
+
# that rejects the `,gpu` model id after we thought we had repaired it.
|
|
81
|
+
#
|
|
82
|
+
# Done ONCE, before the degradation ladder below: the ladder may unregister and
|
|
83
|
+
# re-register several times, and killing the serve on each attempt would only
|
|
84
|
+
# lengthen the window with no model server.
|
|
85
|
+
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
86
|
+
|
|
87
|
+
# Belt and braces: kill any litert-lm process still listening on the port. Scoped
|
|
88
|
+
# to THIS user's processes whose command line carries our port, so a developer's
|
|
89
|
+
# unrelated python is never touched.
|
|
90
|
+
try {
|
|
91
|
+
Get-CimInstance Win32_Process -Filter "Name = 'python.exe' OR Name = 'litert-lm.exe' OR Name = 'pythonw.exe'" -ErrorAction SilentlyContinue |
|
|
92
|
+
Where-Object { $_.CommandLine -and $_.CommandLine -match 'litert-lm' -and $_.CommandLine -match ('--port\s+' + [regex]::Escape($Port)) } |
|
|
93
|
+
ForEach-Object {
|
|
94
|
+
Write-Host "stopping previous litert-lm serve (pid $($_.ProcessId)) before re-registering"
|
|
95
|
+
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
96
|
+
}
|
|
97
|
+
} catch { }
|
|
98
|
+
|
|
99
|
+
# Wait for the port to free, so the Start-ScheduledTask below does not race a
|
|
100
|
+
# dying serve.
|
|
101
|
+
for ($i = 0; $i -lt 15; $i++) {
|
|
102
|
+
$inUse = $null
|
|
103
|
+
try { $inUse = Get-NetTCPConnection -LocalPort ([int]$Port) -State Listen -ErrorAction SilentlyContinue } catch { }
|
|
104
|
+
if (-not $inUse) { break }
|
|
105
|
+
Start-Sleep -Seconds 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$Landed = $null
|
|
109
|
+
foreach ($attempt in $Attempts) {
|
|
110
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
111
|
+
try {
|
|
112
|
+
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $attempt.Triggers -Settings $Settings `
|
|
113
|
+
-Principal (New-LitertPrincipal $attempt.LogonType) -Description $Description -ErrorAction Stop | Out-Null
|
|
114
|
+
$Landed = $attempt
|
|
115
|
+
break
|
|
116
|
+
} catch {
|
|
117
|
+
Write-Host "Registration attempt '$($attempt.Label)' was refused: $($_.Exception.Message)"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (-not $Landed) {
|
|
121
|
+
Write-Error "Scheduled task '$TaskName' could not be registered in ANY supported shape."
|
|
122
|
+
exit 1
|
|
123
|
+
}
|
|
33
124
|
|
|
34
125
|
$registered = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
35
126
|
if (-not $registered) {
|
|
36
127
|
Write-Error "Scheduled task '$TaskName' did NOT register (Get-ScheduledTask found nothing)."
|
|
37
128
|
exit 1
|
|
38
129
|
}
|
|
130
|
+
$definition = Export-ScheduledTask -TaskName $TaskName
|
|
131
|
+
$logonType = if ($definition -match "<LogonType>([^<]+)</LogonType>") { $Matches[1] } else { "unknown" }
|
|
132
|
+
if (($definition -notmatch "<BootTrigger>") -and ($definition -notmatch "<Repetition>")) {
|
|
133
|
+
Write-Warning "Scheduled task '$TaskName' registered with a LOGON-ONLY trigger - the connector would come back after a reboot with no model server."
|
|
134
|
+
}
|
|
135
|
+
if ($logonType -eq "InteractiveToken") {
|
|
136
|
+
Write-Warning "Scheduled task '$TaskName' registered with an INTERACTIVE principal - the model server will only run while $UserId is logged on."
|
|
137
|
+
}
|
|
39
138
|
|
|
40
|
-
# Start it now (don't wait for the next
|
|
139
|
+
# Start it now (don't wait for the next boot) so first-run sensing has a server.
|
|
41
140
|
Start-ScheduledTask -TaskName $TaskName
|
|
42
|
-
Write-Host "Registered scheduled task '$TaskName' (litert-lm serve on :$Port, restarts on failure)."
|
|
141
|
+
Write-Host "Registered scheduled task '$TaskName' (litert-lm serve on :$Port, shape '$($Landed.Label)', logon type $logonType, restarts on failure)."
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<#
|
|
2
|
-
register-task.ps1
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
register-task.ps1 -- register the Scheduled Task that runs the Trillboards
|
|
3
|
+
local-first connector HEADLESS: an AtStartup trigger plus an S4U principal, so
|
|
4
|
+
sensing is back within a minute of an unattended Windows Update reboot with
|
|
5
|
+
NOBODY logged on. Still no admin/UAC -- S4U registers for the CURRENT user and
|
|
6
|
+
stores no password. Invoked by the NSIS installer post-copy.
|
|
6
7
|
#>
|
|
7
8
|
param(
|
|
8
9
|
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
@@ -22,18 +23,108 @@ $wscript = Join-Path $env:WINDIR "System32\wscript.exe"
|
|
|
22
23
|
# crashes without exposing cmd.exe or PowerShell to the customer.
|
|
23
24
|
$argument = '"' + $taskHost + '" "' + $Mode + '"'
|
|
24
25
|
$Action = New-ScheduledTaskAction -Execute $wscript -Argument $argument -WorkingDirectory $InstallDir
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
# NOT AT LOGON. A store back-office PC reboots itself for Windows Update and
|
|
28
|
+
# nobody signs in for days. With the old -AtLogOn trigger sensing simply stopped
|
|
29
|
+
# until someone logged in, and nothing reported it: the machine is up, the
|
|
30
|
+
# customer sees no error, and we have no coverage.
|
|
31
|
+
#
|
|
32
|
+
# The REPEATING trigger is the load-bearing one, not the boot trigger.
|
|
33
|
+
# Microsoft documents that only a member of the Administrators group can create
|
|
34
|
+
# a task with a boot trigger, and installer.nsi:33 is RequestExecutionLevel
|
|
35
|
+
# user -- so on the supported no-admin path -AtStartup can be refused outright.
|
|
36
|
+
# A -Once trigger with a repetition needs no admin, and under the S4U principal
|
|
37
|
+
# it starts the connector with NOBODY logged on within one interval of a
|
|
38
|
+
# reboot. MultipleInstances IgnoreNew (below) makes every repeat a no-op while
|
|
39
|
+
# the connector is already running, so this costs nothing at steady state.
|
|
40
|
+
$RepeatTrigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(1)) `
|
|
41
|
+
-RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650)
|
|
42
|
+
# The boot trigger is a LATENCY optimisation on top: ~30s after boot instead of
|
|
43
|
+
# up to 5 minutes, for installs that do happen to run elevated. The ladder below
|
|
44
|
+
# DROPS it rather than failing when it is refused.
|
|
45
|
+
$BootTrigger = New-ScheduledTaskTrigger -AtStartup
|
|
46
|
+
# Best-effort: the delay is a nicety, StartWhenAvailable + RestartCount are what
|
|
47
|
+
# actually cover a slow boot. Never let it be the thing that fails registration.
|
|
48
|
+
try { $BootTrigger.Delay = "PT30S" } catch { Write-Warning "Could not set the boot-trigger delay: $($_.Exception.Message)" }
|
|
49
|
+
# Kept as a trigger, not a replacement. It is the fastest re-arm when someone
|
|
50
|
+
# does log on, which is the one thing the old logon-only registration was good at.
|
|
51
|
+
$LogonTrigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId
|
|
52
|
+
|
|
53
|
+
# MultipleInstances IgnoreNew is load-bearing now that two triggers exist:
|
|
54
|
+
# logging on while the boot instance is running must NOT start a second
|
|
55
|
+
# connector against the same cameras, config and log file.
|
|
26
56
|
$Settings = New-ScheduledTaskSettingsSet -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1) `
|
|
27
57
|
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
|
|
28
|
-
-
|
|
29
|
-
|
|
58
|
+
-MultipleInstances IgnoreNew `
|
|
59
|
+
-ExecutionTimeLimit ([TimeSpan]::Zero) # no time limit -- sensing runs continuously
|
|
60
|
+
|
|
61
|
+
# WHY S4U AND NOT SYSTEM. S4U ("run whether the user is logged on or not", no
|
|
62
|
+
# stored password) is the only logon type that runs with no interactive session
|
|
63
|
+
# AND keeps the connector inside the SAME user profile. That second half is not
|
|
64
|
+
# a nicety: the device identity, the paired data dir and the ~2.4 GB Gemma model
|
|
65
|
+
# all resolve through os.homedir()/$env:USERPROFILE --
|
|
66
|
+
# edge-core/src/DeviceIdentity.ts:17 os.homedir()/.trillboards
|
|
67
|
+
# edge-sdk/src/config.ts:502 dataDir = USERPROFILE/.trillboards
|
|
68
|
+
# setup-litertlm.ps1:23,25 USERPROFILE/.local/bin, USERPROFILE/.litert-lm
|
|
69
|
+
# Under SYSTEM or NETWORK SERVICE that root becomes
|
|
70
|
+
# C:\Windows\system32\config\systemprofile, so the connector would start at boot,
|
|
71
|
+
# find no device token and no model, and fail while looking alive -- strictly
|
|
72
|
+
# worse than the outage it replaces. SYSTEM also needs admin to register, which
|
|
73
|
+
# would put UAC in front of a deliberately no-admin installer.
|
|
74
|
+
# Password logon is rejected outright: it needs the customer's password at
|
|
75
|
+
# install time and leaves a reusable credential in the task store.
|
|
76
|
+
# S4U's documented limitation is that the token carries no NETWORK credentials.
|
|
77
|
+
# That costs us nothing: the connector only makes unauthenticated-at-the-Windows
|
|
78
|
+
# -layer connections -- outbound TLS to api.trillboards.com, RTSP/ONVIF to LAN
|
|
79
|
+
# cameras (camera creds live in trillboards.config.json, not in the Windows
|
|
80
|
+
# token), a spawned ffmpeg, and loopback to litert-lm on 127.0.0.1.
|
|
81
|
+
function New-ConnectorPrincipal([string]$LogonType) {
|
|
82
|
+
New-ScheduledTaskPrincipal -UserId $UserId -LogonType $LogonType -RunLevel Limited
|
|
83
|
+
}
|
|
30
84
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
$Description = "Trillboards Edge - CCTV $Mode connector"
|
|
86
|
+
|
|
87
|
+
# DEGRADATION LADDER. Each rung drops exactly one capability that a locked-down
|
|
88
|
+
# or non-elevated machine can refuse, in order of how much it costs us. The
|
|
89
|
+
# first rung that registers wins.
|
|
90
|
+
#
|
|
91
|
+
# Measured on windows-latest (run 31836760844), NOT assumed:
|
|
92
|
+
# elevated -> rung 1. Both capabilities available.
|
|
93
|
+
# unelevated -> rung 3. BOTH S4U rungs are refused with "Access is denied",
|
|
94
|
+
# because S4U performs a batch logon and Task Scheduler grants
|
|
95
|
+
# SeBatchLogonRight only to an administrator CALLER (boot
|
|
96
|
+
# triggers are Administrators-only for the same reason).
|
|
97
|
+
# So rung 3 is what a plain no-admin install gets, and it is NOT headless: it
|
|
98
|
+
# still needs somebody logged on. It is nonetheless better than the pre-fix
|
|
99
|
+
# shape, because the repetition revives a connector that died while a user was
|
|
100
|
+
# logged on. ensure-sensing.ps1 asks for the one elevation that lifts a machine
|
|
101
|
+
# from rung 3 to rung 1. Rung 4 exists only so a machine can never be left with
|
|
102
|
+
# NO task at all -- a total sensing outage rather than a degraded one.
|
|
103
|
+
$Attempts = @(
|
|
104
|
+
@{ Label = "S4U + boot + repeat"; LogonType = "S4U"; Triggers = @($BootTrigger, $RepeatTrigger, $LogonTrigger) },
|
|
105
|
+
@{ Label = "S4U + repeat"; LogonType = "S4U"; Triggers = @($RepeatTrigger, $LogonTrigger) },
|
|
106
|
+
@{ Label = "Interactive + repeat"; LogonType = "Interactive"; Triggers = @($RepeatTrigger, $LogonTrigger) },
|
|
107
|
+
@{ Label = "Interactive + logon only"; LogonType = "Interactive"; Triggers = @($LogonTrigger) }
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
$Landed = $null
|
|
111
|
+
foreach ($attempt in $Attempts) {
|
|
112
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
113
|
+
try {
|
|
114
|
+
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $attempt.Triggers -Settings $Settings `
|
|
115
|
+
-Principal (New-ConnectorPrincipal $attempt.LogonType) -Description $Description -ErrorAction Stop | Out-Null
|
|
116
|
+
$Landed = $attempt
|
|
117
|
+
break
|
|
118
|
+
} catch {
|
|
119
|
+
Write-Host "Registration attempt '$($attempt.Label)' was refused: $($_.Exception.Message)"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (-not $Landed) {
|
|
123
|
+
throw "Scheduled task '$TaskName' could not be registered in ANY supported shape."
|
|
124
|
+
}
|
|
34
125
|
|
|
35
126
|
# VERIFY the registration landed instead of trusting Register-ScheduledTask's
|
|
36
|
-
# silence
|
|
127
|
+
# silence -- a silently-missing task is exactly how a real 16-camera install
|
|
37
128
|
# ended up paired with zero frames (the pairing helper then claimed success).
|
|
38
129
|
# Callers (installer.nsi, trillboards-pair.cmd's self-repair) branch on our
|
|
39
130
|
# exit code, so fail LOUDLY here.
|
|
@@ -41,4 +132,63 @@ $registered = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinu
|
|
|
41
132
|
if (-not $registered) {
|
|
42
133
|
throw "Scheduled task '$TaskName' did NOT register (Get-ScheduledTask found nothing)."
|
|
43
134
|
}
|
|
44
|
-
|
|
135
|
+
|
|
136
|
+
# And verify the HEADLESS SHAPE landed, not merely that a task exists. A start
|
|
137
|
+
# path that did not persist is the original defect wearing a new name, and it
|
|
138
|
+
# would be invisible until the next unattended reboot. "Headless" here means two
|
|
139
|
+
# independent things, and BOTH are required: a trigger that fires without anyone
|
|
140
|
+
# logging on (boot or repetition), AND a principal that does not need an
|
|
141
|
+
# interactive session (anything but InteractiveToken).
|
|
142
|
+
$definition = Export-ScheduledTask -TaskName $TaskName
|
|
143
|
+
$logonType = if ($definition -match "<LogonType>([^<]+)</LogonType>") { $Matches[1] } else { "unknown" }
|
|
144
|
+
$hasLogonFreeTrigger = ($definition -match "<BootTrigger>") -or ($definition -match "<Repetition>")
|
|
145
|
+
if (-not $hasLogonFreeTrigger) {
|
|
146
|
+
# Only the last rung of the ladder can get here.
|
|
147
|
+
Write-Warning "Scheduled task '$TaskName' registered with a LOGON-ONLY trigger. Sensing will not resume after an unattended reboot until someone signs in."
|
|
148
|
+
}
|
|
149
|
+
if ($logonType -eq "InteractiveToken") {
|
|
150
|
+
Write-Warning "Scheduled task '$TaskName' registered with an INTERACTIVE principal. Sensing will only run while $UserId is logged on."
|
|
151
|
+
}
|
|
152
|
+
Write-Host "Registered scheduled task '$TaskName' (runs $Mode, shape '$($Landed.Label)', logon type $logonType, restarts on failure)."
|
|
153
|
+
|
|
154
|
+
# HEADLESS COSTS ONE PRIVILEGE, and that is a Windows rule rather than a choice.
|
|
155
|
+
# Measured on windows-latest (run 31836760844): a standard user registering for
|
|
156
|
+
# ITSELF is refused S4U with "Access is denied". S4U performs a BATCH logon, and
|
|
157
|
+
# Task Scheduler only grants the account SeBatchLogonRight when the CALLER is an
|
|
158
|
+
# administrator; boot triggers are Administrators-only for the same reason. So an
|
|
159
|
+
# unelevated install lands on 'Interactive + repeat' and is NOT headless.
|
|
160
|
+
#
|
|
161
|
+
# This script does NOT prompt for elevation -- it runs from install-update.ps1 and
|
|
162
|
+
# from the connector's own repair path, where a UAC dialog would be wrong or
|
|
163
|
+
# impossible. ensure-sensing.ps1 owns that prompt, at wizard close, once.
|
|
164
|
+
# What we DO here is use elevation when we already have it: repair the model
|
|
165
|
+
# server in the same elevated pass, so one prompt fixes both. The two must be
|
|
166
|
+
# headless TOGETHER -- auto-sense waits on 127.0.0.1:9379, so a headless
|
|
167
|
+
# connector paired with a logon-only model server is a connector that boots and
|
|
168
|
+
# senses nothing.
|
|
169
|
+
function Test-IsElevated {
|
|
170
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
171
|
+
(New-Object System.Security.Principal.WindowsPrincipal($identity)).IsInRole(
|
|
172
|
+
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Test-IsElevated) {
|
|
176
|
+
$litertTask = Get-ScheduledTask -TaskName "TrillboardsLiteRtLm" -ErrorAction SilentlyContinue
|
|
177
|
+
if ($litertTask) {
|
|
178
|
+
$litertXml = Export-ScheduledTask -TaskName "TrillboardsLiteRtLm"
|
|
179
|
+
$litertHeadless = (($litertXml -match "<BootTrigger>") -or ($litertXml -match "<Repetition>")) -and
|
|
180
|
+
($litertXml -notmatch "<LogonType>InteractiveToken</LogonType>")
|
|
181
|
+
if (-not $litertHeadless) {
|
|
182
|
+
# Recover the interpreter path from the task we are replacing rather than
|
|
183
|
+
# re-deriving it: setup-litertlm.ps1 resolved it under the USER profile,
|
|
184
|
+
# and an elevated re-derivation could resolve somewhere else entirely.
|
|
185
|
+
$litertExe = ($litertTask.Actions | Select-Object -First 1).Execute
|
|
186
|
+
$litertRegister = Join-Path $InstallDir "bin\register-litertlm-task.ps1"
|
|
187
|
+
if ($litertExe -and (Test-Path $litertRegister)) {
|
|
188
|
+
Write-Host "Repairing 'TrillboardsLiteRtLm' in the same elevated pass."
|
|
189
|
+
try { & $litertRegister -LitertLmPath $litertExe | Out-Null }
|
|
190
|
+
catch { Write-Warning "Could not repair the model-server task: $($_.Exception.Message)" }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -9,6 +9,15 @@ param(
|
|
|
9
9
|
[string]$ModelId = "gemma-4-E2B-it.litertlm",
|
|
10
10
|
[string]$HfRepo = "litert-community/gemma-4-E2B-it-litert-lm",
|
|
11
11
|
[string]$LitertPython = $(if ($env:LITERT_LM_PYTHON) { $env:LITERT_LM_PYTHON } else { "3.12" }),
|
|
12
|
+
# PINNED litert-lm - see deploy/macos/scripts/setup-litertlm.sh for why this is
|
|
13
|
+
# a correctness pin and not hygiene: the GPU delegate is selected by posting a
|
|
14
|
+
# "<model-id>,gpu" model id, upstream calls that syntax deprecated, and 0.14.0
|
|
15
|
+
# removed the parse outright. An unpinned --upgrade lets a customer's per-frame
|
|
16
|
+
# cost move 1.7-6.6x on a reinstall we did not ship. Kept in lockstep with the
|
|
17
|
+
# macOS script, LiteRtVlmProvider's LITERT_LM_PINNED_VERSION and
|
|
18
|
+
# .github/contracts/litert-lm-tested-versions.json by the litert-lm-version-pin
|
|
19
|
+
# guard.
|
|
20
|
+
[string]$LitertLmVersion = $(if ($env:LITERT_LM_VERSION) { $env:LITERT_LM_VERSION } else { "0.16.0" }),
|
|
12
21
|
[string]$ModelVersion = $(if ($env:GEMMA_VLM_VERSION) { $env:GEMMA_VLM_VERSION } else { "1.0.0" }),
|
|
13
22
|
[string]$BundledModelPath = "",
|
|
14
23
|
[string]$BundledModelSha256 = "181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c",
|
|
@@ -109,19 +118,19 @@ function Test-ServeHealthy {
|
|
|
109
118
|
|
|
110
119
|
# WHICH DELEGATE WILL THIS HOST RUN?
|
|
111
120
|
#
|
|
112
|
-
# litert-lm chooses its accelerator by MODEL ID, not by a flag:
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# accelerator 'auto' - the default).
|
|
121
|
+
# litert-lm chooses its accelerator by MODEL ID, not by a flag: the GPU delegate
|
|
122
|
+
# is selected by posting the imported id with a ",gpu" suffix, and the connector
|
|
123
|
+
# ATTEMPTS that id (LiteRtVlmProvider, accelerator 'auto' - the default).
|
|
116
124
|
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
+
# THE MODEL LIST IS NOT THE ANSWER. Measured 2026-08-14 against real serves:
|
|
126
|
+
# litert-lm 0.13.1 lists both ids; 0.14.0 lists one and rejects the ",gpu" POST
|
|
127
|
+
# with 404; 0.16.0 lists one and SERVES the ",gpu" POST on the GPU. So a missing
|
|
128
|
+
# entry means nothing about this host on its own, and the line below used to
|
|
129
|
+
# state flatly that such a machine ran on CPU - which is how a 32-camera field
|
|
130
|
+
# test on connector 0.2.13 reported a CPU-speed result (~2.4 analyses/min) and
|
|
131
|
+
# read it as the product's speed. Log the list, name the version, and let the
|
|
132
|
+
# connector's attempt settle the delegate. installerLocalAiFlow.test.ts asserts
|
|
133
|
+
# that sentence stays gone rather than merely de-prioritised.
|
|
125
134
|
#
|
|
126
135
|
# Never throws: a diagnostic must not be able to fail an install.
|
|
127
136
|
function Get-AdvertisedModelIds {
|
|
@@ -145,9 +154,13 @@ function Write-DelegateDiagnostic {
|
|
|
145
154
|
Log ("delegate: /v1/models advertises " + $ids.Count + " id(s): " + ($ids -join ", "))
|
|
146
155
|
$gpuId = "$ModelId,gpu"
|
|
147
156
|
if ($ids -contains $gpuId) {
|
|
148
|
-
Log "delegate: GPU variant '$gpuId'
|
|
157
|
+
Log "delegate: GPU variant '$gpuId' is listed; the connector attempts it on its first frame"
|
|
149
158
|
} else {
|
|
150
|
-
|
|
159
|
+
# NOT a statement about this host. litert-lm 0.15+ omits this entry while
|
|
160
|
+
# still serving the id (measured on 0.16.0), so the only thing that settles
|
|
161
|
+
# it is the connector's own attempt, reported on its [sensing] line.
|
|
162
|
+
Log ("delegate: '$gpuId' is not listed by litert-lm $LitertLmVersion, which does not mean it is unavailable - " +
|
|
163
|
+
"0.15+ omits the entry and still serves the id. The connector attempts it on its first frame and logs the outcome.")
|
|
151
164
|
}
|
|
152
165
|
}
|
|
153
166
|
|
|
@@ -232,11 +245,48 @@ if (-not [Environment]::Is64BitOperatingSystem) {
|
|
|
232
245
|
exit 1
|
|
233
246
|
}
|
|
234
247
|
|
|
235
|
-
|
|
248
|
+
$litertlmCmd = Get-Command litert-lm -ErrorAction SilentlyContinue
|
|
249
|
+
$litertlmPath = if ($litertlmCmd) { $litertlmCmd.Source } else { Join-Path $LocalBin "litert-lm.exe" }
|
|
250
|
+
function Test-LitertLmCli {
|
|
251
|
+
if (-not (Test-Path -LiteralPath $litertlmPath)) { return $false }
|
|
252
|
+
try {
|
|
253
|
+
& $litertlmPath list *> $null
|
|
254
|
+
return ($LASTEXITCODE -eq 0)
|
|
255
|
+
} catch {
|
|
256
|
+
return $false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function Get-LitertLmVersion {
|
|
261
|
+
if (-not (Test-Path -LiteralPath $litertlmPath)) { return "" }
|
|
262
|
+
try {
|
|
263
|
+
$out = (& $litertlmPath --version 2>$null | Out-String).Trim()
|
|
264
|
+
if (-not $out) { return "" }
|
|
265
|
+
return ($out -split '\s+')[-1]
|
|
266
|
+
} catch {
|
|
267
|
+
return ""
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# A HEALTHY SERVE IS NOT ENOUGH TO SKIP PROVISIONING.
|
|
272
|
+
#
|
|
273
|
+
# `Test-ServeHealthy` substring-matches the bare model id, and every litert-lm
|
|
274
|
+
# version answers that identically - including the ones that reject the `,gpu`
|
|
275
|
+
# model id the GPU delegate is selected with. So this fast path, which exists to
|
|
276
|
+
# make a repeat run cheap, was also the path that let an already-installed
|
|
277
|
+
# 0.14.0 declare itself ready forever: the hosts that most need the pinned
|
|
278
|
+
# runtime are exactly the ones with a serve already answering. The version is
|
|
279
|
+
# therefore part of the short-circuit condition, not a check further down that a
|
|
280
|
+
# healthy serve never reaches.
|
|
281
|
+
$installedLitertLmVersion = Get-LitertLmVersion
|
|
282
|
+
if ((Test-ServeHealthy) -and ($installedLitertLmVersion -eq $LitertLmVersion)) {
|
|
236
283
|
Complete-Setup
|
|
237
284
|
Stop-SetupLock
|
|
238
285
|
exit 0
|
|
239
286
|
}
|
|
287
|
+
if ((Test-ServeHealthy) -and ($installedLitertLmVersion -ne $LitertLmVersion)) {
|
|
288
|
+
Log "serve is healthy but litert-lm is $(if ($installedLitertLmVersion) { $installedLitertLmVersion } else { 'unknown' }) not pinned $LitertLmVersion; reinstalling and restarting the serve"
|
|
289
|
+
}
|
|
240
290
|
|
|
241
291
|
$env:Path = "$LocalBin;$env:Path"
|
|
242
292
|
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
|
|
@@ -253,24 +303,20 @@ if (-not $uvCmd) {
|
|
|
253
303
|
exit 1
|
|
254
304
|
}
|
|
255
305
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
} catch {
|
|
264
|
-
return $false
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (Test-LitertLmCli) {
|
|
269
|
-
Log "litert-lm CLI is healthy; keeping current install"
|
|
306
|
+
# The version is part of "healthy". A list-only check kept each machine on
|
|
307
|
+
# whatever it first installed, which is exactly how the accelerator behaviour
|
|
308
|
+
# came to differ between our boxes and every host provisioned after 2026-07-08.
|
|
309
|
+
# Re-read it: uv may have appeared on PATH since the first read above.
|
|
310
|
+
$installedLitertLmVersion = Get-LitertLmVersion
|
|
311
|
+
if ((Test-LitertLmCli) -and ($installedLitertLmVersion -eq $LitertLmVersion)) {
|
|
312
|
+
Log "litert-lm CLI is healthy at pinned $LitertLmVersion; keeping current install"
|
|
270
313
|
} else {
|
|
271
|
-
|
|
314
|
+
if ($installedLitertLmVersion -and ($installedLitertLmVersion -ne $LitertLmVersion)) {
|
|
315
|
+
Log "litert-lm $installedLitertLmVersion differs from pinned $LitertLmVersion; reinstalling"
|
|
316
|
+
}
|
|
317
|
+
Log "installing or repairing litert-lm==$LitertLmVersion with Python $LitertPython"
|
|
272
318
|
Write-SetupProgress -Stage "local_ai" -Title "Installing local AI" -Message "Installing the local AI engine. Cloud inference remains off." -Phase "installing_runtime" -Percent 10
|
|
273
|
-
& $uvCmd.Source tool install --python $LitertPython --managed-python --
|
|
319
|
+
& $uvCmd.Source tool install --python $LitertPython --managed-python --force "litert-lm==$LitertLmVersion" *>> $LogPath
|
|
274
320
|
$litertlmCmd = Get-Command litert-lm -ErrorAction SilentlyContinue
|
|
275
321
|
$litertlmPath = if ($litertlmCmd) { $litertlmCmd.Source } else { Join-Path $LocalBin "litert-lm.exe" }
|
|
276
322
|
if (-not (Test-LitertLmCli)) {
|
|
@@ -279,7 +325,7 @@ if (Test-LitertLmCli) {
|
|
|
279
325
|
Stop-SetupLock
|
|
280
326
|
exit 1
|
|
281
327
|
}
|
|
282
|
-
Log "litert-lm CLI repaired successfully"
|
|
328
|
+
Log "litert-lm CLI repaired successfully at $(Get-LitertLmVersion)"
|
|
283
329
|
}
|
|
284
330
|
|
|
285
331
|
$bundledModelVerified = $false
|
|
@@ -92,10 +92,19 @@ export interface SensingAcceleratorHealth {
|
|
|
92
92
|
*/
|
|
93
93
|
delegate: 'gpu' | 'cpu' | 'mixed' | null;
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
95
|
+
* Has EVERY reporting feed actually completed an inference under the delegate
|
|
96
|
+
* above? Pessimistic like the rest of this fold: one unconfirmed feed makes
|
|
97
|
+
* the whole verdict unconfirmed, because "we are asking for the GPU" and "we
|
|
98
|
+
* are running on the GPU" are different claims and only the second one is
|
|
99
|
+
* worth printing without a qualifier.
|
|
100
|
+
*/
|
|
101
|
+
delegateConfirmed: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Did `GET /v1/models` list the `,gpu` id? Read ONLY to explain a demotion, so
|
|
104
|
+
* null (nobody asked) is the NORMAL healthy value and must never be rendered
|
|
105
|
+
* as "this host has no GPU". False here means the catalogue omitted it at the
|
|
106
|
+
* moment a demotion was being explained — which on litert-lm 0.15+ is true
|
|
107
|
+
* even on hosts where the id works perfectly. See LiteRtVlmProvider's header.
|
|
99
108
|
*/
|
|
100
109
|
gpuAdvertised: boolean | null;
|
|
101
110
|
/** True once ANY feed's GPU variant failed at runtime and fell back to CPU. */
|