@trillboards/edge-sdk 0.2.156 → 0.2.157

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,128 @@ 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
+ $toolDir = (& $UvPath tool dir 2>$null | Select-Object -First 1)
350
+ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolDir)) { throw 'Cannot resolve uv tool dir' }
351
+ $venv = [IO.Path]::GetFullPath((Join-Path $toolDir.Trim() 'litert-lm'))
352
+ $py = Join-Path $venv 'Scripts\python.exe'
353
+ if (-not (Test-Path -LiteralPath $py -PathType Leaf)) { throw 'The installed litert-lm Python is missing' }
354
+ $site = (& $py -c "import sysconfig;print(sysconfig.get_paths()['purelib'])" 2>$null | Select-Object -First 1)
355
+ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($site)) { throw 'Cannot resolve litert-lm site-packages' }
356
+ $site = [IO.Path]::GetFullPath($site.Trim())
357
+ if (-not $site.StartsWith(($venv.TrimEnd('\') + '\'), [StringComparison]::OrdinalIgnoreCase) -or
358
+ -not (Test-Path -LiteralPath $site -PathType Container)) { throw 'The runtime site is outside its managed venv' }
359
+ return [pscustomobject]@{ Source = $shim; Python = $py; Site = $site }
360
+ }
361
+
362
+ function Get-LitertlmShimFiles($Paths) {
363
+ return @(
364
+ [pscustomobject]@{ Name = 'trillboards_litertlm_shim.py'; Bytes = [IO.File]::ReadAllBytes($Paths.Source) },
365
+ [pscustomobject]@{ Name = 'trillboards-litertlm-shim.pth'; Bytes = [Text.Encoding]::ASCII.GetBytes("import trillboards_litertlm_shim`r`n") }
366
+ )
367
+ }
368
+
369
+ function Get-ShimBytesHash([byte[]]$Bytes) {
370
+ $hasher = [Security.Cryptography.SHA256]::Create()
371
+ try { return ([BitConverter]::ToString($hasher.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() }
372
+ finally { $hasher.Dispose() }
373
+ }
374
+
375
+ function Get-LitertlmShimState([string]$UvPath) {
376
+ $paths = Get-LitertlmShimPaths $UvPath
377
+ $needsRefresh = $false
378
+ foreach ($file in (Get-LitertlmShimFiles $paths)) {
379
+ $target = Join-Path $paths.Site $file.Name
380
+ if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { $needsRefresh = $true; continue }
381
+ if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { $needsRefresh = $true }
382
+ }
383
+ return [pscustomobject]@{ version = 1; needsRefresh = $needsRefresh }
384
+ }
385
+
386
+ function Install-LitertlmShim {
387
+ param([string]$UvPath, [string]$BackupDirectory = '', [switch]$Restore)
388
+ $ErrorActionPreference = 'Stop'
389
+ try {
390
+ $paths = Get-LitertlmShimPaths $UvPath
391
+ $files = @(Get-LitertlmShimFiles $paths)
392
+ $receiptPath = if ($BackupDirectory) { Join-Path $BackupDirectory 'runtime-backup.json' } else { '' }
393
+ if ($Restore) {
394
+ if (-not $receiptPath) { throw 'Runtime rollback requires its backup directory' }
395
+ $receipt = Get-Content -LiteralPath $receiptPath -Raw | ConvertFrom-Json
396
+ if ($receipt.version -ne 1 -or $receipt.site -ine $paths.Site -or @($receipt.files).Count -ne $files.Count) { throw 'Runtime rollback scope mismatch' }
397
+ # Validate the COMPLETE restore before changing either file. Refuse an
398
+ # unexpected concurrent write rather than overwriting another repair.
399
+ foreach ($file in $files) {
400
+ $entry = @($receipt.files | Where-Object { $_.name -eq $file.Name })
401
+ if ($entry.Count -ne 1) { throw 'Runtime rollback file set mismatch' }
402
+ $target = Join-Path $paths.Site $file.Name
403
+ if (Test-Path -LiteralPath $target) {
404
+ $current = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
405
+ if ($current -ne $entry[0].installedSha256 -and $current -ne $entry[0].previousSha256) { throw 'Runtime changed after the update; rollback refused' }
406
+ }
407
+ 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' }
408
+ }
409
+ foreach ($entry in $receipt.files) {
410
+ $target = Join-Path $paths.Site $entry.name
411
+ if ($entry.existed) { Copy-Item -LiteralPath (Join-Path $BackupDirectory $entry.name) -Destination $target -Force }
412
+ elseif (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force }
413
+ if ($entry.existed) {
414
+ if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry.previousSha256) { throw 'Restored runtime hash mismatch' }
415
+ } elseif (Test-Path -LiteralPath $target) { throw 'Runtime rollback left a newly introduced file' }
416
+ }
417
+ Log 'restored the previous litert-lm shim bytes'
418
+ return $true
419
+ }
420
+ if ($BackupDirectory) {
421
+ if (Test-Path -LiteralPath $receiptPath) { throw 'Preserve the existing runtime rollback receipt' }
422
+ New-Item -ItemType Directory -Force -Path $BackupDirectory | Out-Null
423
+ $backups = @(foreach ($file in $files) {
424
+ $target = Join-Path $paths.Site $file.Name
425
+ $existed = Test-Path -LiteralPath $target -PathType Leaf
426
+ $previousSha = $null
427
+ if ($existed) {
428
+ $previousSha = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
429
+ $backup = Join-Path $BackupDirectory $file.Name
430
+ Copy-Item -LiteralPath $target -Destination $backup
431
+ if ((Get-FileHash -LiteralPath $backup -Algorithm SHA256).Hash.ToLowerInvariant() -ne $previousSha) { throw 'Runtime rollback copy did not verify' }
432
+ }
433
+ [pscustomobject]@{ name = $file.Name; existed = [bool]$existed; previousSha256 = $previousSha; installedSha256 = (Get-ShimBytesHash $file.Bytes) }
434
+ })
435
+ [IO.File]::WriteAllText($receiptPath, (@{ version = 1; site = $paths.Site; files = $backups } | ConvertTo-Json -Depth 5), [Text.UTF8Encoding]::new($false))
436
+ }
437
+ foreach ($file in $files) {
438
+ $target = Join-Path $paths.Site $file.Name
439
+ $temporary = "$target.$PID.update"
440
+ try {
441
+ [IO.File]::WriteAllBytes($temporary, $file.Bytes)
442
+ if ((Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { throw 'Runtime refresh hash mismatch' }
443
+ # PS 5.1 binds an ordinary $null to an empty String here, which .NET
444
+ # treats as an invalid backup path. Pass an actual null String.
445
+ if (Test-Path -LiteralPath $target) { [IO.File]::Replace($temporary, $target, [NullString]::Value) }
446
+ else { [IO.File]::Move($temporary, $target) }
447
+ if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { throw 'Installed runtime refresh hash mismatch' }
448
+ } finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force } }
449
+ }
450
+ # A fresh interpreter proves the installed wrapper imports. No engine,
451
+ # weights, image inference, model config or camera credentials are loaded.
452
+ & $paths.Python -c "import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,'_trillboards_litertlm_shim',False) else 1)" *>> $LogPath
453
+ if ($LASTEXITCODE -ne 0) { throw 'The refreshed model-service shim did not import' }
454
+ Log "litert-lm fast-conversation shim active in $($paths.Site)"
455
+ return $true
456
+ } catch {
457
+ Log "WARN: litert-lm shim operation failed: $_"
458
+ return $false
459
+ }
460
+ }
461
+
336
462
  if ($SmokeTest) {
337
463
  if ([string]::IsNullOrWhiteSpace($ProgressPath)) { throw "SmokeTest requires ProgressPath" }
338
464
  Write-SetupProgress -Stage "local_ai" -Title "Installing local AI" -Message "Smoke test" -Phase "installing_model" -Percent 62 -TotalBytes $ExpectedModelBytes
@@ -345,7 +471,10 @@ if ($SmokeTest) {
345
471
  exit 0
346
472
  }
347
473
 
348
- $setupMutex = New-Object System.Threading.Mutex($false, "Local\TrillboardsLiteRtLmSetup")
474
+ # S4U/boot tasks and the interactive installer run in different sessions. A
475
+ # Local mutex cannot serialize them; scope the Global name to this user's SID.
476
+ $setupSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
477
+ $setupMutex = New-Object System.Threading.Mutex($false, ("Global\TrillboardsLiteRtLmSetup-" + $setupSid))
349
478
  $haveSetupMutex = $false
350
479
  try {
351
480
  $haveSetupMutex = $setupMutex.WaitOne(0)
@@ -354,6 +483,10 @@ try {
354
483
  }
355
484
 
356
485
  if (-not $haveSetupMutex) {
486
+ if ($RuntimeAction -ne 'Setup') {
487
+ $setupMutex.Dispose()
488
+ throw 'Local AI setup is busy; no concurrent runtime refresh was attempted'
489
+ }
357
490
  Log "LiteRT-LM provisioning is already running; attaching setup progress"
358
491
  if ([string]::IsNullOrWhiteSpace($ProgressPath) -and -not $RequireBundledModel) {
359
492
  $setupMutex.Dispose()
@@ -389,6 +522,24 @@ function Stop-SetupLock {
389
522
  $setupMutex.Dispose()
390
523
  }
391
524
 
525
+ if ($RuntimeAction -ne 'Setup') {
526
+ try {
527
+ $refreshUvPath = Join-Path $LocalBin 'uv.exe'
528
+ if (-not (Test-Path -LiteralPath $refreshUvPath -PathType Leaf)) {
529
+ $refreshUv = Get-Command uv -ErrorAction Stop
530
+ $refreshUvPath = $refreshUv.Source
531
+ }
532
+ if ($RuntimeAction -eq 'Inspect') {
533
+ Get-LitertlmShimState $refreshUvPath | ConvertTo-Json -Compress
534
+ } else {
535
+ if ([string]::IsNullOrWhiteSpace($RuntimeBackupDir)) { throw 'A runtime rollback directory is required' }
536
+ $refreshArgs = @{ UvPath = $refreshUvPath; BackupDirectory = $RuntimeBackupDir; Restore = ($RuntimeAction -eq 'Restore') }
537
+ if (-not (Install-LitertlmShim @refreshArgs)) { throw 'The local AI runtime refresh/rollback did not verify' }
538
+ }
539
+ } finally { Stop-SetupLock }
540
+ exit 0
541
+ }
542
+
392
543
  Log "starting LiteRT-LM provisioning (Windows x64, CPU backend)"
393
544
  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
545
 
@@ -585,41 +736,6 @@ if ((Test-LitertLmCli) -and ($installedLitertLmVersion -eq $LitertLmVersion)) {
585
736
  # short-circuits that branch on a healthy CLI -- gating this behind it is exactly
586
737
  # how a repaired-and-healthy machine would keep the slow serve and report fine.
587
738
  # 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
- }
622
- }
623
739
  Install-LitertlmShim -UvPath $uvCmd.Source | Out-Null
624
740
 
625
741
  $bundledModelVerified = $false
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.157';
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.157";
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.157';
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.157",
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",