@trillboards/edge-sdk 0.2.156 → 0.2.158

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.
@@ -499,7 +499,7 @@ function Invoke-Commit {
499
499
  # failure and $ErrorActionPreference = 'Stop' turns that into ours.
500
500
  & $installUpdate -InstallDir $InstallDir -StagingDir $PayloadDir
501
501
  } catch {
502
- Fail 'failed' "the swap failed and the previous version was restored: $($_.Exception.Message)"
502
+ Fail 'failed' "the update transaction failed: $($_.Exception.Message)"
503
503
  }
504
504
 
505
505
  # The measured half. The probe predicts; this observes.
@@ -10,9 +10,17 @@ param(
10
10
 
11
11
  $ErrorActionPreference = "Stop"
12
12
  $previousDir = Join-Path $InstallDir ".previous"
13
+ $runtimeRepairMarker = Join-Path $previousDir 'model-runtime-rollback-failed.json'
13
14
  $parts = @("bin", "lib")
14
15
  $movedPrevious = New-Object System.Collections.Generic.List[string]
15
16
  $movedStaged = New-Object System.Collections.Generic.List[string]
17
+ $script:modelRuntimeUpdate = $null
18
+
19
+ # A failed external-runtime rollback owns this recovery tree. A later update
20
+ # must not delete it in the normal .previous cleanup or restart sensing over it.
21
+ if (Test-Path -LiteralPath $runtimeRepairMarker) {
22
+ throw 'The previous local AI rollback needs repair; saved connector/runtime backups were left untouched'
23
+ }
16
24
 
17
25
  function Assert-ConnectorVersion([string]$Root) {
18
26
  $node = Join-Path $Root "bin\node.exe"
@@ -43,6 +51,109 @@ function Register-Connector([string]$Root) {
43
51
  if ($LASTEXITCODE -ne 0) { throw "Scheduled-task registration failed." }
44
52
  }
45
53
 
54
+ function Get-OwnedLocalAiProcesses {
55
+ # Task Scheduler can stop the uv launcher while leaving its Python child
56
+ # alive. Match only this user's model server on the verified task's port;
57
+ # never kill all Python processes or a different user's listener.
58
+ $portPattern = '(?:^|\s)--port(?:\s+|=)' + $script:modelRuntimeUpdate.Port + '(?=\s|$)'
59
+ $processes = Get-CimInstance Win32_Process -Filter "Name = 'python.exe' OR Name = 'pythonw.exe' OR Name = 'litert-lm.exe'" -OperationTimeoutSec 5 -ErrorAction Stop
60
+ foreach ($process in $processes) {
61
+ if (-not $process.CommandLine -or $process.CommandLine -notmatch 'litert[-_]lm' -or
62
+ $process.CommandLine -notmatch '(?:^|\s)serve(?=\s|$)' -or $process.CommandLine -notmatch $portPattern) { continue }
63
+ $owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -OperationTimeoutSec 5 -ErrorAction Stop
64
+ if ($owner.ReturnValue -ne 0 -or -not $owner.Sid) { throw 'Cannot verify the model process owner; no unverified process was stopped' }
65
+ if ($owner.Sid -eq $script:modelRuntimeUpdate.OwnerSid) { $process }
66
+ }
67
+ }
68
+
69
+ function Stop-LocalAiForUpdate {
70
+ Disable-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop | Out-Null
71
+ Stop-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop
72
+ for ($attempt = 0; $attempt -lt 15; $attempt++) {
73
+ $task = Get-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop
74
+ $remaining = @(Get-OwnedLocalAiProcesses)
75
+ foreach ($process in $remaining) { Stop-Process -Id $process.ProcessId -Force -ErrorAction Stop }
76
+ $listenerErrors = @()
77
+ $listeners = @(Get-NetTCPConnection -LocalPort $script:modelRuntimeUpdate.Port -State Listen -ErrorAction SilentlyContinue -ErrorVariable listenerErrors)
78
+ if (@($listenerErrors | Where-Object { $_.CategoryInfo.Category -ne 'ObjectNotFound' }).Count) { throw 'Cannot verify the local AI port is free' }
79
+ if ($task.State -notin @('Running', 'Queued') -and $remaining.Count -eq 0 -and $listeners.Count -eq 0) { return }
80
+ Start-Sleep -Seconds 1
81
+ }
82
+ throw 'The local AI task, server process or listening port did not stop; runtime files were not replaced'
83
+ }
84
+
85
+ function Restore-LocalAiTaskState {
86
+ if ($null -eq $script:modelRuntimeUpdate) { return }
87
+ if ($script:modelRuntimeUpdate.Enabled) {
88
+ Enable-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop | Out-Null
89
+ if ($script:modelRuntimeUpdate.Running) { Start-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop }
90
+ } else {
91
+ Disable-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop | Out-Null
92
+ }
93
+ }
94
+
95
+ function Update-LocalAiRuntime([string]$Root) {
96
+ $modelTaskErrors = @()
97
+ $task = Get-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction SilentlyContinue -ErrorVariable modelTaskErrors
98
+ if (-not $task) {
99
+ # Only a genuine absence belongs to first-time setup. An inaccessible task
100
+ # must not be mistaken for "no model service" and silently keep old code.
101
+ if (@($modelTaskErrors | Where-Object { $_.CategoryInfo.Category -ne 'ObjectNotFound' }).Count) {
102
+ throw 'The local AI task is inaccessible; runtime refresh was not skipped'
103
+ }
104
+ return
105
+ }
106
+ $taskUser = [string]$task.Principal.UserId
107
+ $taskSid = if ($taskUser -match '^S-1-') { $taskUser } else {
108
+ (New-Object Security.Principal.NTAccount($taskUser)).Translate([Security.Principal.SecurityIdentifier]).Value
109
+ }
110
+ $currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
111
+ $action = @($task.Actions)
112
+ $ownTool = Join-Path $env:USERPROFILE '.local\bin\litert-lm.exe'
113
+ if ($taskSid -ne $currentSid -or $action.Count -ne 1 -or
114
+ [IO.Path]::GetFullPath([string]$action[0].Execute) -ine [IO.Path]::GetFullPath($ownTool)) {
115
+ throw 'Cannot verify ownership of the local AI task; no runtime was changed'
116
+ }
117
+ $serverArguments = [string]$action[0].Arguments
118
+ $ports = [regex]::Matches($serverArguments, '(?:^|\s)--port(?:\s+|=)([0-9]+)(?=\s|$)')
119
+ if ($serverArguments -notmatch '^serve\s' -or $ports.Count -ne 1 -or
120
+ [long]$ports[0].Groups[1].Value -lt 1 -or [long]$ports[0].Groups[1].Value -gt 65535) {
121
+ throw 'Cannot verify the local AI task port; no runtime was changed'
122
+ }
123
+ $setup = Join-Path $Root 'bin\setup-litertlm.ps1'
124
+ if (-not (Test-Path -LiteralPath $setup -PathType Leaf)) { throw 'The released local AI setup is missing' }
125
+ # Inspect first: an unchanged shim does NOT restart a healthy model server on
126
+ # every SDK update. /v1/models and the CLI version cannot detect stale code.
127
+ $state = (& $setup -RuntimeAction Inspect | Out-String).Trim() | ConvertFrom-Json
128
+ if ($LASTEXITCODE -ne 0 -or $state.version -ne 1) { throw 'Could not inspect the installed local AI runtime' }
129
+ if (-not $state.needsRefresh) { return }
130
+ $script:modelRuntimeUpdate = [pscustomobject]@{
131
+ Enabled = [bool]$task.Settings.Enabled
132
+ Running = ($task.State -eq 'Running')
133
+ OwnerSid = $currentSid
134
+ Port = [int]$ports[0].Groups[1].Value
135
+ Setup = $setup
136
+ Backup = (Join-Path $previousDir 'litertlm-runtime')
137
+ }
138
+ Stop-LocalAiForUpdate
139
+ # This mode is local file replacement only. It never downloads a CLI/model,
140
+ # imports weights, resets tuning, re-registers tasks or runs image inference.
141
+ & $setup -RuntimeAction Refresh -RuntimeBackupDir $script:modelRuntimeUpdate.Backup
142
+ if ($LASTEXITCODE -ne 0) { throw 'The local AI runtime refresh failed' }
143
+ Restore-LocalAiTaskState
144
+ }
145
+
146
+ function Restore-LocalAiRuntime {
147
+ if ($null -eq $script:modelRuntimeUpdate) { return }
148
+ Stop-LocalAiForUpdate
149
+ $receipt = Join-Path $script:modelRuntimeUpdate.Backup 'runtime-backup.json'
150
+ if (Test-Path -LiteralPath $receipt -PathType Leaf) {
151
+ & $script:modelRuntimeUpdate.Setup -RuntimeAction Restore -RuntimeBackupDir $script:modelRuntimeUpdate.Backup
152
+ if ($LASTEXITCODE -ne 0) { throw 'The previous local AI runtime could not be verified; rollback backup was preserved' }
153
+ }
154
+ Restore-LocalAiTaskState
155
+ }
156
+
46
157
  <#
47
158
  Carry the detector's RAM ceiling onto an EXISTING install.
48
159
 
@@ -192,6 +303,9 @@ try {
192
303
  }
193
304
 
194
305
  Assert-ConnectorVersion $InstallDir
306
+ # The Python venv is OUTSIDE bin/lib. Refresh it transactionally before the
307
+ # connector can review a saved frame with the new native-image contract.
308
+ Update-LocalAiRuntime $InstallDir
195
309
  Register-Connector $InstallDir
196
310
  Assert-ConnectorScheduled
197
311
  if (Test-Path (Join-Path $InstallDir "trillboards.config.json")) {
@@ -206,11 +320,21 @@ try {
206
320
  Write-Host "Trillboards Edge update installed successfully."
207
321
  } catch {
208
322
  $failure = $_
323
+ $runtimeRollbackFailure = $null
209
324
  try {
210
325
  $stopScript = Join-Path $InstallDir "bin\stop-existing.ps1"
211
326
  if (Test-Path $stopScript) { & $stopScript -InstallDir $InstallDir }
212
327
  } catch {}
213
328
 
329
+ # Use the NEW setup while its bin tree still exists. Restore external runtime
330
+ # bytes before rolling bin/lib back; neither path touches pairings or spools.
331
+ try { Restore-LocalAiRuntime } catch { $runtimeRollbackFailure = $_.Exception.Message }
332
+ if ($runtimeRollbackFailure) {
333
+ [IO.File]::WriteAllText($runtimeRepairMarker,
334
+ (@{ version = 1; failedAtUtc = [DateTime]::UtcNow.ToString('o'); detail = $runtimeRollbackFailure } | ConvertTo-Json),
335
+ [Text.UTF8Encoding]::new($false))
336
+ throw "Update failed: $($failure.Exception.Message). Local AI rollback needs repair: $runtimeRollbackFailure. Preserved both connector trees, runtime backups, pairings and saved evidence."
337
+ }
214
338
  foreach ($part in $movedStaged) {
215
339
  Remove-Item -Recurse -Force (Join-Path $InstallDir $part) -ErrorAction SilentlyContinue
216
340
  }
@@ -24,6 +24,10 @@ param(
24
24
  [switch]$RequireBundledModel,
25
25
  [switch]$AllowModelDownload,
26
26
  [string]$ProgressPath = "",
27
+ # Upgrades refresh only our shipped Python adapter, never the CLI, weights,
28
+ # model config or task registration. The caller owns stopping/restarting it.
29
+ [ValidateSet('Setup', 'Inspect', 'Refresh', 'Restore')][string]$RuntimeAction = 'Setup',
30
+ [string]$RuntimeBackupDir = "",
27
31
  [switch]$SmokeTest
28
32
  )
29
33
 
@@ -333,6 +337,134 @@ function Import-VerifiedModel([string]$SourcePath, [string]$ExecutablePath) {
333
337
  return (($LASTEXITCODE -eq 0) -and (Test-LitertLmModel $ExecutablePath))
334
338
  }
335
339
 
340
+ # One owner for the shim on fresh installs AND upgrades. None of these helpers
341
+ # import a model, rewrite model config, install dependencies or register tasks.
342
+ function Get-LitertlmShimPaths([string]$UvPath) {
343
+ $shim = $null
344
+ foreach ($candidate in @(
345
+ (Join-Path $PSScriptRoot 'trillboards_litertlm_shim.py'),
346
+ (Join-Path $PSScriptRoot '..\litertlm\trillboards_litertlm_shim.py')
347
+ )) { if (Test-Path -LiteralPath $candidate -PathType Leaf) { $shim = $candidate; break } }
348
+ if (-not $shim) { throw 'The released litert-lm shim is missing' }
349
+ # PS 5.1 stops a native process when Select-Object -First has its line,
350
+ # turning a successful lookup into exit -1. Drain stdout before selecting.
351
+ $toolDirOutput = @(& $UvPath tool dir 2>$null)
352
+ $toolDirExit = $LASTEXITCODE
353
+ $toolDir = $toolDirOutput | Select-Object -First 1
354
+ if ($toolDirExit -ne 0 -or [string]::IsNullOrWhiteSpace($toolDir)) { throw 'Cannot resolve uv tool dir' }
355
+ $venv = [IO.Path]::GetFullPath((Join-Path $toolDir.Trim() 'litert-lm'))
356
+ $py = Join-Path $venv 'Scripts\python.exe'
357
+ if (-not (Test-Path -LiteralPath $py -PathType Leaf)) { throw 'The installed litert-lm Python is missing' }
358
+ $siteOutput = @(& $py -c "import sysconfig;print(sysconfig.get_paths()['purelib'])" 2>$null)
359
+ $siteExit = $LASTEXITCODE
360
+ $site = $siteOutput | Select-Object -First 1
361
+ if ($siteExit -ne 0 -or [string]::IsNullOrWhiteSpace($site)) { throw 'Cannot resolve litert-lm site-packages' }
362
+ $site = [IO.Path]::GetFullPath($site.Trim())
363
+ if (-not $site.StartsWith(($venv.TrimEnd('\') + '\'), [StringComparison]::OrdinalIgnoreCase) -or
364
+ -not (Test-Path -LiteralPath $site -PathType Container)) { throw 'The runtime site is outside its managed venv' }
365
+ return [pscustomobject]@{ Source = $shim; Python = $py; Site = $site }
366
+ }
367
+
368
+ function Get-LitertlmShimFiles($Paths) {
369
+ return @(
370
+ [pscustomobject]@{ Name = 'trillboards_litertlm_shim.py'; Bytes = [IO.File]::ReadAllBytes($Paths.Source) },
371
+ [pscustomobject]@{ Name = 'trillboards-litertlm-shim.pth'; Bytes = [Text.Encoding]::ASCII.GetBytes("import trillboards_litertlm_shim`r`n") }
372
+ )
373
+ }
374
+
375
+ function Get-ShimBytesHash([byte[]]$Bytes) {
376
+ $hasher = [Security.Cryptography.SHA256]::Create()
377
+ try { return ([BitConverter]::ToString($hasher.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() }
378
+ finally { $hasher.Dispose() }
379
+ }
380
+
381
+ function Get-LitertlmShimState([string]$UvPath) {
382
+ $paths = Get-LitertlmShimPaths $UvPath
383
+ $needsRefresh = $false
384
+ foreach ($file in (Get-LitertlmShimFiles $paths)) {
385
+ $target = Join-Path $paths.Site $file.Name
386
+ if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { $needsRefresh = $true; continue }
387
+ if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { $needsRefresh = $true }
388
+ }
389
+ return [pscustomobject]@{ version = 1; needsRefresh = $needsRefresh }
390
+ }
391
+
392
+ function Install-LitertlmShim {
393
+ param([string]$UvPath, [string]$BackupDirectory = '', [switch]$Restore)
394
+ $ErrorActionPreference = 'Stop'
395
+ try {
396
+ $paths = Get-LitertlmShimPaths $UvPath
397
+ $files = @(Get-LitertlmShimFiles $paths)
398
+ $receiptPath = if ($BackupDirectory) { Join-Path $BackupDirectory 'runtime-backup.json' } else { '' }
399
+ if ($Restore) {
400
+ if (-not $receiptPath) { throw 'Runtime rollback requires its backup directory' }
401
+ $receipt = Get-Content -LiteralPath $receiptPath -Raw | ConvertFrom-Json
402
+ if ($receipt.version -ne 1 -or $receipt.site -ine $paths.Site -or @($receipt.files).Count -ne $files.Count) { throw 'Runtime rollback scope mismatch' }
403
+ # Validate the COMPLETE restore before changing either file. Refuse an
404
+ # unexpected concurrent write rather than overwriting another repair.
405
+ foreach ($file in $files) {
406
+ $entry = @($receipt.files | Where-Object { $_.name -eq $file.Name })
407
+ if ($entry.Count -ne 1) { throw 'Runtime rollback file set mismatch' }
408
+ $target = Join-Path $paths.Site $file.Name
409
+ if (Test-Path -LiteralPath $target) {
410
+ $current = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
411
+ if ($current -ne $entry[0].installedSha256 -and $current -ne $entry[0].previousSha256) { throw 'Runtime changed after the update; rollback refused' }
412
+ }
413
+ if ($entry[0].existed -and (Get-FileHash -LiteralPath (Join-Path $BackupDirectory $file.Name) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry[0].previousSha256) { throw 'Runtime rollback backup hash mismatch' }
414
+ }
415
+ foreach ($entry in $receipt.files) {
416
+ $target = Join-Path $paths.Site $entry.name
417
+ if ($entry.existed) { Copy-Item -LiteralPath (Join-Path $BackupDirectory $entry.name) -Destination $target -Force }
418
+ elseif (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force }
419
+ if ($entry.existed) {
420
+ if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry.previousSha256) { throw 'Restored runtime hash mismatch' }
421
+ } elseif (Test-Path -LiteralPath $target) { throw 'Runtime rollback left a newly introduced file' }
422
+ }
423
+ Log 'restored the previous litert-lm shim bytes'
424
+ return $true
425
+ }
426
+ if ($BackupDirectory) {
427
+ if (Test-Path -LiteralPath $receiptPath) { throw 'Preserve the existing runtime rollback receipt' }
428
+ New-Item -ItemType Directory -Force -Path $BackupDirectory | Out-Null
429
+ $backups = @(foreach ($file in $files) {
430
+ $target = Join-Path $paths.Site $file.Name
431
+ $existed = Test-Path -LiteralPath $target -PathType Leaf
432
+ $previousSha = $null
433
+ if ($existed) {
434
+ $previousSha = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
435
+ $backup = Join-Path $BackupDirectory $file.Name
436
+ Copy-Item -LiteralPath $target -Destination $backup
437
+ if ((Get-FileHash -LiteralPath $backup -Algorithm SHA256).Hash.ToLowerInvariant() -ne $previousSha) { throw 'Runtime rollback copy did not verify' }
438
+ }
439
+ [pscustomobject]@{ name = $file.Name; existed = [bool]$existed; previousSha256 = $previousSha; installedSha256 = (Get-ShimBytesHash $file.Bytes) }
440
+ })
441
+ [IO.File]::WriteAllText($receiptPath, (@{ version = 1; site = $paths.Site; files = $backups } | ConvertTo-Json -Depth 5), [Text.UTF8Encoding]::new($false))
442
+ }
443
+ foreach ($file in $files) {
444
+ $target = Join-Path $paths.Site $file.Name
445
+ $temporary = "$target.$PID.update"
446
+ try {
447
+ [IO.File]::WriteAllBytes($temporary, $file.Bytes)
448
+ if ((Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { throw 'Runtime refresh hash mismatch' }
449
+ # PS 5.1 binds an ordinary $null to an empty String here, which .NET
450
+ # treats as an invalid backup path. Pass an actual null String.
451
+ if (Test-Path -LiteralPath $target) { [IO.File]::Replace($temporary, $target, [NullString]::Value) }
452
+ else { [IO.File]::Move($temporary, $target) }
453
+ if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { throw 'Installed runtime refresh hash mismatch' }
454
+ } finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force } }
455
+ }
456
+ # A fresh interpreter proves the installed wrapper imports. No engine,
457
+ # weights, image inference, model config or camera credentials are loaded.
458
+ & $paths.Python -c "import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,'_trillboards_litertlm_shim',False) else 1)" *>> $LogPath
459
+ if ($LASTEXITCODE -ne 0) { throw 'The refreshed model-service shim did not import' }
460
+ Log "litert-lm fast-conversation shim active in $($paths.Site)"
461
+ return $true
462
+ } catch {
463
+ Log "WARN: litert-lm shim operation failed: $_"
464
+ return $false
465
+ }
466
+ }
467
+
336
468
  if ($SmokeTest) {
337
469
  if ([string]::IsNullOrWhiteSpace($ProgressPath)) { throw "SmokeTest requires ProgressPath" }
338
470
  Write-SetupProgress -Stage "local_ai" -Title "Installing local AI" -Message "Smoke test" -Phase "installing_model" -Percent 62 -TotalBytes $ExpectedModelBytes
@@ -345,7 +477,10 @@ if ($SmokeTest) {
345
477
  exit 0
346
478
  }
347
479
 
348
- $setupMutex = New-Object System.Threading.Mutex($false, "Local\TrillboardsLiteRtLmSetup")
480
+ # S4U/boot tasks and the interactive installer run in different sessions. A
481
+ # Local mutex cannot serialize them; scope the Global name to this user's SID.
482
+ $setupSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
483
+ $setupMutex = New-Object System.Threading.Mutex($false, ("Global\TrillboardsLiteRtLmSetup-" + $setupSid))
349
484
  $haveSetupMutex = $false
350
485
  try {
351
486
  $haveSetupMutex = $setupMutex.WaitOne(0)
@@ -354,6 +489,10 @@ try {
354
489
  }
355
490
 
356
491
  if (-not $haveSetupMutex) {
492
+ if ($RuntimeAction -ne 'Setup') {
493
+ $setupMutex.Dispose()
494
+ throw 'Local AI setup is busy; no concurrent runtime refresh was attempted'
495
+ }
357
496
  Log "LiteRT-LM provisioning is already running; attaching setup progress"
358
497
  if ([string]::IsNullOrWhiteSpace($ProgressPath) -and -not $RequireBundledModel) {
359
498
  $setupMutex.Dispose()
@@ -389,6 +528,24 @@ function Stop-SetupLock {
389
528
  $setupMutex.Dispose()
390
529
  }
391
530
 
531
+ if ($RuntimeAction -ne 'Setup') {
532
+ try {
533
+ $refreshUvPath = Join-Path $LocalBin 'uv.exe'
534
+ if (-not (Test-Path -LiteralPath $refreshUvPath -PathType Leaf)) {
535
+ $refreshUv = Get-Command uv -ErrorAction Stop
536
+ $refreshUvPath = $refreshUv.Source
537
+ }
538
+ if ($RuntimeAction -eq 'Inspect') {
539
+ Get-LitertlmShimState $refreshUvPath | ConvertTo-Json -Compress
540
+ } else {
541
+ if ([string]::IsNullOrWhiteSpace($RuntimeBackupDir)) { throw 'A runtime rollback directory is required' }
542
+ $refreshArgs = @{ UvPath = $refreshUvPath; BackupDirectory = $RuntimeBackupDir; Restore = ($RuntimeAction -eq 'Restore') }
543
+ if (-not (Install-LitertlmShim @refreshArgs)) { throw 'The local AI runtime refresh/rollback did not verify' }
544
+ }
545
+ } finally { Stop-SetupLock }
546
+ exit 0
547
+ }
548
+
392
549
  Log "starting LiteRT-LM provisioning (Windows x64, CPU backend)"
393
550
  Write-SetupProgress -Stage "local_ai" -Title "Installing local AI" -Message "Preparing the private on-device AI runtime. Cloud inference remains off." -Phase "installing_runtime" -Percent 0
394
551
 
@@ -584,43 +741,13 @@ if ((Test-LitertLmCli) -and ($installedLitertLmVersion -eq $LitertLmVersion)) {
584
741
  # --force` rebuilds the venv and takes both files with it, while Test-LitertLmCli
585
742
  # short-circuits that branch on a healthy CLI -- gating this behind it is exactly
586
743
  # how a repaired-and-healthy machine would keep the slow serve and report fine.
587
- # Never throws: this is a speed fix, and it must not be able to fail an install.
588
- function Install-LitertlmShim {
589
- param([string]$UvPath)
590
- try {
591
- $shim = $null
592
- foreach ($c in @(
593
- (Join-Path $PSScriptRoot "trillboards_litertlm_shim.py"),
594
- (Join-Path $PSScriptRoot "..\litertlm\trillboards_litertlm_shim.py")
595
- )) { if (Test-Path -LiteralPath $c -PathType Leaf) { $shim = $c; break } }
596
- if (-not $shim) { Log "WARN: litert-lm shim source not found; serve keeps stock (slower) conversation setup"; return $false }
597
-
598
- $toolDir = (& $UvPath tool dir 2>$null | Select-Object -First 1)
599
- if ([string]::IsNullOrWhiteSpace($toolDir)) { Log "WARN: could not resolve 'uv tool dir'; skipping litert-lm shim"; return $false }
600
- $py = Join-Path $toolDir.Trim() "litert-lm\Scripts\python.exe"
601
- if (-not (Test-Path -LiteralPath $py -PathType Leaf)) { Log "WARN: litert-lm venv python missing at $py; skipping shim"; return $false }
602
-
603
- $site = (& $py -c "import sysconfig;print(sysconfig.get_paths()['purelib'])" 2>$null | Select-Object -First 1)
604
- if ([string]::IsNullOrWhiteSpace($site) -or -not (Test-Path -LiteralPath $site.Trim())) {
605
- Log "WARN: could not resolve litert-lm site-packages; skipping shim"; return $false
606
- }
607
- $site = $site.Trim()
608
- Copy-Item -LiteralPath $shim -Destination (Join-Path $site "trillboards_litertlm_shim.py") -Force
609
- Set-Content -LiteralPath (Join-Path $site "trillboards-litertlm-shim.pth") -Value "import trillboards_litertlm_shim" -Encoding ASCII
610
-
611
- # Capability is established by ATTEMPTING THE OPERATION. Importing litert_lm
612
- # here is the only thing that proves the wrap landed on the class the serve
613
- # will use; a file that merely exists proves nothing.
614
- & $py -c "import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,'_trillboards_litertlm_shim',False) else 1)" *>> $LogPath
615
- if ($LASTEXITCODE -eq 0) { Log "litert-lm fast-conversation shim active in $site"; return $true }
616
- Log "WARN: litert-lm shim did not take effect; serve keeps stock (slower) conversation setup"
617
- return $false
618
- } catch {
619
- Log "WARN: litert-lm shim install failed: $_"
620
- return $false
621
- }
744
+ # The adapter also binds runtime model proof to each response; it is required
745
+ # before readiness probes can verify the installed model.
746
+ if (-not (Install-LitertlmShim -UvPath $uvCmd.Source)) {
747
+ Fail-LocalAi "The local AI runtime adapter could not be installed. See $LogPath."
748
+ Stop-SetupLock
749
+ exit 1
622
750
  }
623
- Install-LitertlmShim -UvPath $uvCmd.Source | Out-Null
624
751
 
625
752
  $bundledModelVerified = $false
626
753
  if (-not [string]::IsNullOrWhiteSpace($BundledModelPath)) {
package/dist/EdgeAgent.js CHANGED
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
44
44
  const CommandRouter_1 = require("./CommandRouter");
45
45
  const config_1 = require("./config");
46
46
  const dataDir_1 = require("./dataDir");
47
- const SDK_VERSION = '0.2.156';
47
+ const SDK_VERSION = '0.2.158';
48
48
  class EdgeAgent extends events_1.EventEmitter {
49
49
  constructor(config, deps) {
50
50
  super();
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
31
31
  skipped: number;
32
32
  };
33
33
  }
34
- declare const SDK_VERSION = "0.2.156";
34
+ declare const SDK_VERSION = "0.2.158";
35
35
  declare function checkNodeVersion(): CheckResult;
36
36
  declare function checkPlatform(): CheckResult;
37
37
  declare function checkMemory(): CheckResult;
package/dist/diagnose.js CHANGED
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
63
63
  // ---------------------------------------------------------------------------
64
64
  // SDK version — mirrors package.json; kept in sync by CI
65
65
  // ---------------------------------------------------------------------------
66
- const SDK_VERSION = '0.2.156';
66
+ const SDK_VERSION = '0.2.158';
67
67
  exports._SDK_VERSION = SDK_VERSION;
68
68
  // ---------------------------------------------------------------------------
69
69
  // ANSI helpers (no external deps)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trillboards/edge-sdk",
3
- "version": "0.2.156",
3
+ "version": "0.2.158",
4
4
  "description": "Composable cross-platform Edge AI SDK for audience sensing on DOOH/CTV devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",