sensorium-mcp 3.1.3-board.3 → 3.1.3-board.5
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/Install-Sensorium.ps1 +200 -49
- package/dist/board/components/Card.js +10 -1
- package/dist/board/components/Combobox.js +78 -0
- package/dist/board/components/CommentComposer.js +4 -1
- package/dist/board/components/Lane.js +6 -1
- package/dist/board/components/StateBanner.js +5 -3
- package/dist/board/components/StatusChip.js +1 -0
- package/dist/board/components/rateLimit.js +19 -0
- package/dist/board/mock/fixtures.js +34 -0
- package/dist/board/schema/policy.js +137 -72
- package/dist/board/styles/board.css +460 -3
- package/dist/board/views/BoardView.js +8 -2
- package/dist/board/views/BuilderView.js +334 -29
- package/dist/board/views/CardView.js +36 -17
- package/dist/config/agent-settings.d.ts +9 -0
- package/dist/config/agent-settings.d.ts.map +1 -1
- package/dist/config/agent-settings.js +56 -0
- package/dist/config/agent-settings.js.map +1 -1
- package/dist/config/detail-grant.d.ts +7 -16
- package/dist/config/detail-grant.d.ts.map +1 -1
- package/dist/config/detail-grant.js +12 -35
- package/dist/config/detail-grant.js.map +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/misc-settings.d.ts +8 -1
- package/dist/config/misc-settings.d.ts.map +1 -1
- package/dist/config/misc-settings.js +40 -3
- package/dist/config/misc-settings.js.map +1 -1
- package/dist/config/operator-grant.d.ts +9 -0
- package/dist/config/operator-grant.d.ts.map +1 -0
- package/dist/config/operator-grant.js +24 -0
- package/dist/config/operator-grant.js.map +1 -0
- package/dist/config/voice-grant.d.ts +10 -0
- package/dist/config/voice-grant.d.ts.map +1 -0
- package/dist/config/voice-grant.js +18 -0
- package/dist/config/voice-grant.js.map +1 -0
- package/dist/dashboard/presets.d.ts +1 -1
- package/dist/dashboard/presets.d.ts.map +1 -1
- package/dist/dashboard/presets.js +1 -1
- package/dist/dashboard/presets.js.map +1 -1
- package/dist/dashboard/routes/board.d.ts +2 -0
- package/dist/dashboard/routes/board.d.ts.map +1 -1
- package/dist/dashboard/routes/board.js +171 -0
- package/dist/dashboard/routes/board.js.map +1 -1
- package/dist/dashboard/routes/claude.d.ts.map +1 -1
- package/dist/dashboard/routes/claude.js +1 -10
- package/dist/dashboard/routes/claude.js.map +1 -1
- package/dist/dashboard/routes/codex.d.ts +14 -0
- package/dist/dashboard/routes/codex.d.ts.map +1 -0
- package/dist/dashboard/routes/codex.js +92 -0
- package/dist/dashboard/routes/codex.js.map +1 -0
- package/dist/dashboard/routes/prompts.d.ts +2 -2
- package/dist/dashboard/routes/prompts.d.ts.map +1 -1
- package/dist/dashboard/routes/prompts.js +2 -2
- package/dist/dashboard/routes/prompts.js.map +1 -1
- package/dist/dashboard/routes/threads-read.d.ts +10 -7
- package/dist/dashboard/routes/threads-read.d.ts.map +1 -1
- package/dist/dashboard/routes/threads-read.js +21 -16
- package/dist/dashboard/routes/threads-read.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +5 -1
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/spa.html +10 -10
- package/dist/data/memory/bootstrap.d.ts +1 -8
- package/dist/data/memory/bootstrap.d.ts.map +1 -1
- package/dist/data/memory/bootstrap.js +27 -37
- package/dist/data/memory/bootstrap.js.map +1 -1
- package/dist/data/memory/consolidation.d.ts +20 -1
- package/dist/data/memory/consolidation.d.ts.map +1 -1
- package/dist/data/memory/consolidation.js +53 -3
- package/dist/data/memory/consolidation.js.map +1 -1
- package/dist/data/memory/index.d.ts +0 -4
- package/dist/data/memory/index.d.ts.map +1 -1
- package/dist/data/memory/index.js +0 -4
- package/dist/data/memory/index.js.map +1 -1
- package/dist/data/memory/key-knowledge.d.ts +51 -0
- package/dist/data/memory/key-knowledge.d.ts.map +1 -0
- package/dist/data/memory/key-knowledge.js +128 -0
- package/dist/data/memory/key-knowledge.js.map +1 -0
- package/dist/data/memory/migration-runner.d.ts +1 -1
- package/dist/data/memory/migration-runner.d.ts.map +1 -1
- package/dist/data/memory/migration-runner.js +19 -1
- package/dist/data/memory/migration-runner.js.map +1 -1
- package/dist/data/memory/narrative-lint.d.ts +2 -2
- package/dist/data/memory/narrative-lint.d.ts.map +1 -1
- package/dist/data/memory/narrative-lint.js +23 -13
- package/dist/data/memory/narrative-lint.js.map +1 -1
- package/dist/data/memory/narrative-resolutions.d.ts +9 -2
- package/dist/data/memory/narrative-resolutions.d.ts.map +1 -1
- package/dist/data/memory/narrative-resolutions.js +9 -8
- package/dist/data/memory/narrative-resolutions.js.map +1 -1
- package/dist/data/memory/narrative-sources.d.ts +1 -1
- package/dist/data/memory/narrative-sources.d.ts.map +1 -1
- package/dist/data/memory/narrative-sources.js +8 -3
- package/dist/data/memory/narrative-sources.js.map +1 -1
- package/dist/data/memory/narrative-store.d.ts +10 -0
- package/dist/data/memory/narrative-store.d.ts.map +1 -1
- package/dist/data/memory/narrative-store.js +37 -35
- package/dist/data/memory/narrative-store.js.map +1 -1
- package/dist/data/memory/narrative.d.ts.map +1 -1
- package/dist/data/memory/narrative.js +34 -28
- package/dist/data/memory/narrative.js.map +1 -1
- package/dist/data/memory/note-archival.d.ts +1 -1
- package/dist/data/memory/note-archival.d.ts.map +1 -1
- package/dist/data/memory/note-archival.js +7 -3
- package/dist/data/memory/note-archival.js.map +1 -1
- package/dist/data/memory/note-embeddings.d.ts +7 -0
- package/dist/data/memory/note-embeddings.d.ts.map +1 -1
- package/dist/data/memory/note-embeddings.js +6 -2
- package/dist/data/memory/note-embeddings.js.map +1 -1
- package/dist/data/memory/schema-ddl.d.ts +1 -1
- package/dist/data/memory/schema-ddl.d.ts.map +1 -1
- package/dist/data/memory/schema-ddl.js +3 -1
- package/dist/data/memory/schema-ddl.js.map +1 -1
- package/dist/data/memory/semantic-notes.d.ts +13 -8
- package/dist/data/memory/semantic-notes.d.ts.map +1 -1
- package/dist/data/memory/semantic-notes.js +12 -17
- package/dist/data/memory/semantic-notes.js.map +1 -1
- package/dist/data/memory/thread-registry.d.ts +7 -1
- package/dist/data/memory/thread-registry.d.ts.map +1 -1
- package/dist/data/memory/thread-registry.js +14 -5
- package/dist/data/memory/thread-registry.js.map +1 -1
- package/dist/data/memory/utils.d.ts +6 -0
- package/dist/data/memory/utils.d.ts.map +1 -1
- package/dist/data/memory/utils.js +13 -0
- package/dist/data/memory/utils.js.map +1 -1
- package/dist/integrations/telegram/types.d.ts +11 -0
- package/dist/integrations/telegram/types.d.ts.map +1 -1
- package/dist/llm/prompts/narrative.d.ts.map +1 -1
- package/dist/llm/prompts/narrative.js +27 -86
- package/dist/llm/prompts/narrative.js.map +1 -1
- package/dist/services/claude-spawn.service.d.ts.map +1 -1
- package/dist/services/claude-spawn.service.js +6 -1
- package/dist/services/claude-spawn.service.js.map +1 -1
- package/dist/services/codex-spawn.service.d.ts.map +1 -1
- package/dist/services/codex-spawn.service.js +10 -4
- package/dist/services/codex-spawn.service.js.map +1 -1
- package/dist/services/consolidation.service.d.ts.map +1 -1
- package/dist/services/consolidation.service.js +10 -43
- package/dist/services/consolidation.service.js.map +1 -1
- package/dist/services/copilot-spawn.service.d.ts.map +1 -1
- package/dist/services/copilot-spawn.service.js +6 -1
- package/dist/services/copilot-spawn.service.js.map +1 -1
- package/dist/services/daily-session.d.ts +22 -11
- package/dist/services/daily-session.d.ts.map +1 -1
- package/dist/services/daily-session.js +69 -24
- package/dist/services/daily-session.js.map +1 -1
- package/dist/services/dispatcher/broker.d.ts +9 -0
- package/dist/services/dispatcher/broker.d.ts.map +1 -1
- package/dist/services/dispatcher/broker.js.map +1 -1
- package/dist/services/dispatcher/poller.d.ts.map +1 -1
- package/dist/services/dispatcher/poller.js +31 -24
- package/dist/services/dispatcher/poller.js.map +1 -1
- package/dist/services/mcp-config.service.d.ts.map +1 -1
- package/dist/services/mcp-config.service.js +10 -1
- package/dist/services/mcp-config.service.js.map +1 -1
- package/dist/services/memory-briefing.service.d.ts +1 -1
- package/dist/services/memory-briefing.service.d.ts.map +1 -1
- package/dist/services/memory-briefing.service.js +28 -73
- package/dist/services/memory-briefing.service.js.map +1 -1
- package/dist/services/memory-pruning.service.d.ts.map +1 -1
- package/dist/services/memory-pruning.service.js +22 -4
- package/dist/services/memory-pruning.service.js.map +1 -1
- package/dist/services/predefined-prompts.d.ts +11 -0
- package/dist/services/predefined-prompts.d.ts.map +1 -0
- package/dist/services/predefined-prompts.js +25 -0
- package/dist/services/predefined-prompts.js.map +1 -0
- package/dist/services/spawn-common.d.ts +10 -2
- package/dist/services/spawn-common.d.ts.map +1 -1
- package/dist/services/spawn-common.js +14 -4
- package/dist/services/spawn-common.js.map +1 -1
- package/dist/sessions/mcp-registry.d.ts +7 -2
- package/dist/sessions/mcp-registry.d.ts.map +1 -1
- package/dist/sessions/mcp-registry.js +52 -30
- package/dist/sessions/mcp-registry.js.map +1 -1
- package/dist/tools/defs/memory-defs.d.ts.map +1 -1
- package/dist/tools/defs/memory-defs.js +12 -4
- package/dist/tools/defs/memory-defs.js.map +1 -1
- package/dist/tools/defs/session-defs.js +2 -2
- package/dist/tools/defs/session-defs.js.map +1 -1
- package/dist/tools/defs/utility-defs.js +1 -1
- package/dist/tools/defs/utility-defs.js.map +1 -1
- package/dist/tools/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory-tools.js +66 -9
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/session-tools.js +3 -10
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +13 -1
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/utility-tools.d.ts.map +1 -1
- package/dist/tools/utility-tools.js +31 -0
- package/dist/tools/utility-tools.js.map +1 -1
- package/dist/tools/wait/drive-handler.d.ts.map +1 -1
- package/dist/tools/wait/drive-handler.js +18 -14
- package/dist/tools/wait/drive-handler.js.map +1 -1
- package/dist/tools/wait/message-delivery.d.ts +9 -6
- package/dist/tools/wait/message-delivery.d.ts.map +1 -1
- package/dist/tools/wait/message-delivery.js +71 -65
- package/dist/tools/wait/message-delivery.js.map +1 -1
- package/dist/tools/wait/message-processing.d.ts.map +1 -1
- package/dist/tools/wait/message-processing.js +2 -1
- package/dist/tools/wait/message-processing.js.map +1 -1
- package/dist/tools/wait/poll-loop.d.ts.map +1 -1
- package/dist/tools/wait/poll-loop.js +6 -0
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/package.json +1 -1
- package/templates/increment-pr-pipeline.default.md +79 -0
- package/dist/data/memory/reflection-dedup.d.ts +0 -17
- package/dist/data/memory/reflection-dedup.d.ts.map +0 -1
- package/dist/data/memory/reflection-dedup.js +0 -55
- package/dist/data/memory/reflection-dedup.js.map +0 -1
- package/dist/data/memory/reflection-gates.d.ts +0 -40
- package/dist/data/memory/reflection-gates.d.ts.map +0 -1
- package/dist/data/memory/reflection-gates.js +0 -220
- package/dist/data/memory/reflection-gates.js.map +0 -1
- package/dist/data/memory/reflection-sources.d.ts +0 -13
- package/dist/data/memory/reflection-sources.d.ts.map +0 -1
- package/dist/data/memory/reflection-sources.js +0 -115
- package/dist/data/memory/reflection-sources.js.map +0 -1
- package/dist/data/memory/reflection.d.ts +0 -43
- package/dist/data/memory/reflection.d.ts.map +0 -1
- package/dist/data/memory/reflection.js +0 -329
- package/dist/data/memory/reflection.js.map +0 -1
- package/dist/llm/prompts/reflection.d.ts +0 -6
- package/dist/llm/prompts/reflection.d.ts.map +0 -1
- package/dist/llm/prompts/reflection.js +0 -77
- package/dist/llm/prompts/reflection.js.map +0 -1
- package/templates/atomic-build-spec.default.md +0 -57
- package/templates/daily-review.default.md +0 -26
- package/templates/fire-and-forget.default.md +0 -76
- package/templates/offload.default.md +0 -33
- package/templates/phased-build.default.md +0 -59
- package/templates/sensorium-repair.default.md +0 -114
package/Install-Sensorium.ps1
CHANGED
|
@@ -117,8 +117,27 @@ Save-Config $Config
|
|
|
117
117
|
function Stop-SupervisorProcess {
|
|
118
118
|
$procs = Get-Process -Name "sensorium-supervisor" -ErrorAction SilentlyContinue
|
|
119
119
|
if ($procs) {
|
|
120
|
+
# Keep the original PID set. A name-only lookup after stopping can confuse a
|
|
121
|
+
# newly launched process with the instance we were asked to stop.
|
|
122
|
+
$targetPids = @($procs | Select-Object -ExpandProperty Id)
|
|
120
123
|
Write-Host "Stopping background sensorium-supervisor process(es)..."
|
|
121
|
-
$procs | Stop-Process -Force
|
|
124
|
+
$procs | Stop-Process -Force -ErrorAction Stop
|
|
125
|
+
|
|
126
|
+
# Stop-Process returns once a signal has been sent, not once all image/file
|
|
127
|
+
# handles are released. Do not attempt an in-place replacement until every
|
|
128
|
+
# PID we targeted is gone; a continuing process makes the update unsafe.
|
|
129
|
+
$deadline = (Get-Date).AddSeconds(10)
|
|
130
|
+
do {
|
|
131
|
+
$remainingPids = @($targetPids | Where-Object {
|
|
132
|
+
Get-Process -Id $_ -ErrorAction SilentlyContinue
|
|
133
|
+
})
|
|
134
|
+
if ($remainingPids.Count -eq 0) { break }
|
|
135
|
+
Start-Sleep -Milliseconds 250
|
|
136
|
+
} while ((Get-Date) -lt $deadline)
|
|
137
|
+
|
|
138
|
+
if ($remainingPids.Count -gt 0) {
|
|
139
|
+
throw "Refusing to update: sensorium-supervisor PID(s) $($remainingPids -join ', ') did not stop within 10 seconds."
|
|
140
|
+
}
|
|
122
141
|
Write-Host "Process(es) stopped." -ForegroundColor Yellow
|
|
123
142
|
}
|
|
124
143
|
}
|
|
@@ -227,7 +246,45 @@ function Save-BinaryMetadata {
|
|
|
227
246
|
InstalledAt = (Get-Date).ToString("o")
|
|
228
247
|
}
|
|
229
248
|
|
|
230
|
-
|
|
249
|
+
# Do not write metadata in place: a failure halfway through a write would make a
|
|
250
|
+
# good binary look stale on the next run. The temp name is unique so concurrent
|
|
251
|
+
# or previously interrupted installs cannot collide with it.
|
|
252
|
+
$metadataTempPath = "$MetadataPath.tmp.$PID.$([guid]::NewGuid().ToString('N'))"
|
|
253
|
+
try {
|
|
254
|
+
$metadata | ConvertTo-Json | Set-Content -LiteralPath $metadataTempPath -Encoding ASCII
|
|
255
|
+
Move-FileWithRetry -Source $metadataTempPath -Destination $MetadataPath -Operation "update release metadata"
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (Test-Path -LiteralPath $metadataTempPath) {
|
|
259
|
+
Remove-Item -LiteralPath $metadataTempPath -Force -ErrorAction SilentlyContinue
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function Move-FileWithRetry {
|
|
265
|
+
param(
|
|
266
|
+
[string]$Source,
|
|
267
|
+
[string]$Destination,
|
|
268
|
+
[string]$Operation = "replace file",
|
|
269
|
+
[int]$MaxAttempts = 20,
|
|
270
|
+
[int]$RetryDelayMs = 1500
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
|
274
|
+
try {
|
|
275
|
+
Move-Item -LiteralPath $Source -Destination $Destination -Force -ErrorAction Stop
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
if ($attempt -eq $MaxAttempts) {
|
|
280
|
+
throw "Failed to $Operation at $Destination after $MaxAttempts attempts (~$([math]::Round($MaxAttempts * $RetryDelayMs / 1000))s). Original error: $_"
|
|
281
|
+
}
|
|
282
|
+
if ($attempt % 4 -eq 0) {
|
|
283
|
+
Write-Host " ... waiting to $Operation (attempt $attempt/$MaxAttempts)..." -ForegroundColor Yellow
|
|
284
|
+
}
|
|
285
|
+
Start-Sleep -Milliseconds $RetryDelayMs
|
|
286
|
+
}
|
|
287
|
+
}
|
|
231
288
|
}
|
|
232
289
|
|
|
233
290
|
function Get-BinaryAsset {
|
|
@@ -244,7 +301,13 @@ function Get-BinaryAsset {
|
|
|
244
301
|
$headers = Get-GithubAuthHeaders -Accept "application/octet-stream"
|
|
245
302
|
Write-Host "Downloading $($AssetInfo.AssetName) ..."
|
|
246
303
|
|
|
247
|
-
$
|
|
304
|
+
$operationId = "$PID.$([guid]::NewGuid().ToString('N'))"
|
|
305
|
+
$tmpFile = "$Destination.tmp.$operationId"
|
|
306
|
+
$backupPath = "$Destination.bak.$operationId"
|
|
307
|
+
$metadataBackupPath = "$MetadataPath.bak.$operationId"
|
|
308
|
+
$hadBinary = Test-Path -LiteralPath $Destination
|
|
309
|
+
$hadMetadata = Test-Path -LiteralPath $MetadataPath
|
|
310
|
+
$swapped = $false
|
|
248
311
|
try {
|
|
249
312
|
Invoke-WebRequest -Uri $downloadUrl -Headers $headers -OutFile $tmpFile -UseBasicParsing -ErrorAction Stop
|
|
250
313
|
}
|
|
@@ -256,15 +319,74 @@ function Get-BinaryAsset {
|
|
|
256
319
|
throw "Download failed: $_"
|
|
257
320
|
}
|
|
258
321
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
Copy-Item $Destination $backupPath -Force
|
|
322
|
+
try {
|
|
323
|
+
# Preserve both halves of the installed state. If anything after the
|
|
324
|
+
# executable swap fails (notably metadata persistence), restore the pair.
|
|
325
|
+
if ($hadBinary) { Copy-Item -LiteralPath $Destination -Destination $backupPath -Force -ErrorAction Stop }
|
|
326
|
+
if ($hadMetadata) { Copy-Item -LiteralPath $MetadataPath -Destination $metadataBackupPath -Force -ErrorAction Stop }
|
|
327
|
+
|
|
328
|
+
# The old exe can still be locked for a while after Stop-Process — AV/EDR
|
|
329
|
+
# may retain its image-section handle during a post-exit scan.
|
|
330
|
+
Move-FileWithRetry -Source $tmpFile -Destination $Destination -Operation "replace supervisor binary"
|
|
331
|
+
$swapped = $true
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
Unblock-File -Path $Destination -ErrorAction Stop
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
Write-Host "[WARN] Could not unblock ${Destination}: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Save-BinaryMetadata -MetadataPath $MetadataPath -AssetInfo $AssetInfo
|
|
341
|
+
Write-Host "Binary placed at: $Destination" -ForegroundColor Green
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
$updateError = $_
|
|
345
|
+
if ($swapped) {
|
|
346
|
+
Write-Host "[WARN] Update failed after binary replacement; restoring the previous binary and metadata..." -ForegroundColor Yellow
|
|
347
|
+
$restoreErrors = @()
|
|
348
|
+
try {
|
|
349
|
+
if ($hadBinary) {
|
|
350
|
+
$restoreFile = "$Destination.restore.$operationId"
|
|
351
|
+
Copy-Item -LiteralPath $backupPath -Destination $restoreFile -Force -ErrorAction Stop
|
|
352
|
+
Move-FileWithRetry -Source $restoreFile -Destination $Destination -Operation "restore previous supervisor binary"
|
|
353
|
+
}
|
|
354
|
+
elseif (Test-Path -LiteralPath $Destination) {
|
|
355
|
+
Remove-Item -LiteralPath $Destination -Force -ErrorAction Stop
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch { $restoreErrors += "binary: $($_.Exception.Message)" }
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
if ($hadMetadata) {
|
|
362
|
+
# Stage the backup under a unique name, then use the same
|
|
363
|
+
# retrying replacement path as the binary. Copy-Item directly
|
|
364
|
+
# to $MetadataPath could leave metadata restoration vulnerable
|
|
365
|
+
# to a locked destination and bypass the retry policy.
|
|
366
|
+
$metadataRestoreFile = "$MetadataPath.restore.$operationId"
|
|
367
|
+
Copy-Item -LiteralPath $metadataBackupPath -Destination $metadataRestoreFile -Force -ErrorAction Stop
|
|
368
|
+
Move-FileWithRetry -Source $metadataRestoreFile -Destination $MetadataPath -Operation "restore previous release metadata"
|
|
369
|
+
}
|
|
370
|
+
elseif (Test-Path -LiteralPath $MetadataPath) {
|
|
371
|
+
Remove-Item -LiteralPath $MetadataPath -Force -ErrorAction Stop
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch { $restoreErrors += "metadata: $($_.Exception.Message)" }
|
|
375
|
+
|
|
376
|
+
if ($restoreErrors.Count -gt 0) {
|
|
377
|
+
throw "Update failed and recovery was incomplete ($($restoreErrors -join '; ')). Original error: $($updateError.Exception.Message)"
|
|
378
|
+
}
|
|
379
|
+
throw "Update failed after replacement; the previous binary and metadata were restored. Original error: $($updateError.Exception.Message)"
|
|
380
|
+
}
|
|
381
|
+
throw
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
foreach ($path in @($tmpFile, $backupPath, $metadataBackupPath, "$Destination.restore.$operationId", "$MetadataPath.restore.$operationId")) {
|
|
385
|
+
if (Test-Path -LiteralPath $path) {
|
|
386
|
+
Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
|
|
387
|
+
}
|
|
388
|
+
}
|
|
263
389
|
}
|
|
264
|
-
Move-Item $tmpFile $Destination -Force
|
|
265
|
-
Unblock-File -Path $Destination -ErrorAction SilentlyContinue
|
|
266
|
-
Save-BinaryMetadata -MetadataPath $MetadataPath -AssetInfo $AssetInfo
|
|
267
|
-
Write-Host "Binary placed at: $Destination" -ForegroundColor Green
|
|
268
390
|
}
|
|
269
391
|
|
|
270
392
|
function Install-StartupLauncher {
|
|
@@ -439,7 +561,7 @@ function Sync-ReleaseBinary {
|
|
|
439
561
|
Write-Host "Fetching release via SecureVault (profile: $SecureVaultProfile)..."
|
|
440
562
|
securevault run pwsh -NoProfile -File "`"$PSCommandPath`"" -SyncReleaseOnly --profile $SecureVaultProfile
|
|
441
563
|
if ($LASTEXITCODE -ne 0) {
|
|
442
|
-
throw "SecureVault-driven release sync failed (exit $LASTEXITCODE).
|
|
564
|
+
throw "SecureVault-driven release sync failed (exit $LASTEXITCODE). See the error above for the actual cause (e.g. a locked binary, network failure, or a missing GitHub PAT for profile '$SecureVaultProfile' -> `$env:SENSORIUM_GH_TOKEN)."
|
|
443
565
|
}
|
|
444
566
|
}
|
|
445
567
|
|
|
@@ -451,47 +573,76 @@ if ($SyncReleaseOnly) {
|
|
|
451
573
|
}
|
|
452
574
|
|
|
453
575
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
|
454
|
-
|
|
576
|
+
# Serialize only full installer invocations for this Windows user. The
|
|
577
|
+
# SecureVault child above deliberately returns before this point, so it can do
|
|
578
|
+
# the authenticated release sync without waiting on (and deadlocking with) its
|
|
579
|
+
# parent installer process.
|
|
580
|
+
$currentUserSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
581
|
+
$installMutexName = "Local\SensoriumSupervisor.Install.$($currentUserSid -replace '[^A-Za-z0-9_.-]', '_')"
|
|
582
|
+
$installMutex = [System.Threading.Mutex]::new($false, $installMutexName)
|
|
583
|
+
$mutexHeld = $false
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
try {
|
|
587
|
+
$mutexHeld = $installMutex.WaitOne([TimeSpan]::FromMinutes(5))
|
|
588
|
+
}
|
|
589
|
+
catch [System.Threading.AbandonedMutexException] {
|
|
590
|
+
# The prior installer died without releasing the mutex; Windows has given
|
|
591
|
+
# us ownership, so continue while making that state visible to the user.
|
|
592
|
+
$mutexHeld = $true
|
|
593
|
+
Write-Host "[WARN] Recovered an abandoned installer mutex; continuing." -ForegroundColor Yellow
|
|
594
|
+
}
|
|
455
595
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
Write-Host "=== Sensorium Supervisor INSTALL ===" -ForegroundColor Cyan
|
|
462
|
-
}
|
|
463
|
-
Write-Host "SecureVault profile: $(if ([string]::IsNullOrWhiteSpace($SecureVaultProfile)) { '<disabled>' } else { $SecureVaultProfile })"
|
|
464
|
-
Write-Host "Watcher mode: $UpdateMode"
|
|
465
|
-
Write-Host "MCP start command: $EffectiveMCPStartCommand"
|
|
466
|
-
Write-Host "MCP runtime isolation: $(if ($IsolateMcpRuntime) { 'ENABLED' } else { 'disabled (legacy npx)' })"
|
|
467
|
-
Write-Host "Binary : $Binary"
|
|
468
|
-
Write-Host ""
|
|
469
|
-
|
|
470
|
-
# Step 1 — stop existing instance
|
|
471
|
-
if ($alreadyExists) {
|
|
472
|
-
Stop-SupervisorProcess
|
|
473
|
-
}
|
|
596
|
+
if (-not $mutexHeld) {
|
|
597
|
+
throw "Another Sensorium install/update is still running for this user. Timed out waiting for it to finish."
|
|
598
|
+
}
|
|
474
599
|
|
|
475
|
-
|
|
476
|
-
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
|
|
600
|
+
$alreadyExists = Test-Path $Binary
|
|
477
601
|
|
|
478
|
-
|
|
479
|
-
|
|
602
|
+
Write-Host ""
|
|
603
|
+
if ($alreadyExists) {
|
|
604
|
+
Write-Host "=== Sensorium Supervisor UPDATE ===" -ForegroundColor Cyan
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
Write-Host "=== Sensorium Supervisor INSTALL ===" -ForegroundColor Cyan
|
|
608
|
+
}
|
|
609
|
+
Write-Host "SecureVault profile: $(if ([string]::IsNullOrWhiteSpace($SecureVaultProfile)) { '<disabled>' } else { $SecureVaultProfile })"
|
|
610
|
+
Write-Host "Watcher mode: $UpdateMode"
|
|
611
|
+
Write-Host "MCP start command: $EffectiveMCPStartCommand"
|
|
612
|
+
Write-Host "MCP runtime isolation: $(if ($IsolateMcpRuntime) { 'ENABLED' } else { 'disabled (legacy npx)' })"
|
|
613
|
+
Write-Host "Binary : $Binary"
|
|
614
|
+
Write-Host ""
|
|
480
615
|
|
|
481
|
-
# Step
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
# so the install degrades safely to legacy behaviour.
|
|
485
|
-
if ($IsolateMcpRuntime) {
|
|
486
|
-
if (-not (Initialize-IsolatedMcpRuntime)) {
|
|
487
|
-
$IsolateMcpRuntime = $false
|
|
488
|
-
Write-Host "[WARN] Isolation disabled for this run — using the shared npm cache / inherited cwd." -ForegroundColor Yellow
|
|
616
|
+
# Step 1 — stop existing instance
|
|
617
|
+
if ($alreadyExists) {
|
|
618
|
+
Stop-SupervisorProcess
|
|
489
619
|
}
|
|
490
|
-
}
|
|
491
620
|
|
|
492
|
-
# Step
|
|
493
|
-
|
|
494
|
-
|
|
621
|
+
# Step 2 — ensure bin dir exists
|
|
622
|
+
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
|
|
623
|
+
|
|
624
|
+
# Step 3 — fetch release metadata and download only when needed
|
|
625
|
+
Sync-ReleaseBinary
|
|
495
626
|
|
|
496
|
-
# Step
|
|
497
|
-
|
|
627
|
+
# Step 3b — prepare the isolated MCP runtime area (opt-in): a private npm cache +
|
|
628
|
+
# repo-independent cwd. The `npx ...@latest` command is unchanged (auto-update
|
|
629
|
+
# preserved). On any failure this disables the isolation env in Start-Supervisor,
|
|
630
|
+
# so the install degrades safely to legacy behaviour.
|
|
631
|
+
if ($IsolateMcpRuntime) {
|
|
632
|
+
if (-not (Initialize-IsolatedMcpRuntime)) {
|
|
633
|
+
$IsolateMcpRuntime = $false
|
|
634
|
+
Write-Host "[WARN] Isolation disabled for this run — using the shared npm cache / inherited cwd." -ForegroundColor Yellow
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
# Step 4 — install launcher and start
|
|
639
|
+
Install-StartupLauncher
|
|
640
|
+
Start-Supervisor
|
|
641
|
+
|
|
642
|
+
# Step 5 — verify
|
|
643
|
+
Show-Status
|
|
644
|
+
}
|
|
645
|
+
finally {
|
|
646
|
+
if ($mutexHeld) { $installMutex.ReleaseMutex() }
|
|
647
|
+
if ($installMutex) { $installMutex.Dispose() }
|
|
648
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import StatusChip from "./StatusChip.js";
|
|
8
8
|
import PriorityChip from "./PriorityChip.js";
|
|
9
|
+
import { providerLabel, retryLabel } from "./rateLimit.js";
|
|
9
10
|
|
|
10
11
|
export default {
|
|
11
12
|
name: "Card",
|
|
@@ -21,7 +22,11 @@ export default {
|
|
|
21
22
|
const mins = Math.max(0, Math.round((Date.now() - new Date(props.card.startedAt)) / 60000));
|
|
22
23
|
return mins >= 60 ? Math.floor(mins / 60) + "h " + (mins % 60) + "m" : mins + "m";
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
function rlText() {
|
|
26
|
+
const p = providerLabel(props.card);
|
|
27
|
+
return (p ? p + " · " : "") + retryLabel(props.card);
|
|
28
|
+
}
|
|
29
|
+
return { emit, elapsed, rlText };
|
|
25
30
|
},
|
|
26
31
|
template: `
|
|
27
32
|
<article class="card" role="listitem" tabindex="0"
|
|
@@ -29,6 +34,10 @@ export default {
|
|
|
29
34
|
@keydown.enter="emit('open', card)">
|
|
30
35
|
<h3 class="card__title">{{ card.title }}</h3>
|
|
31
36
|
<p v-if="card.description" class="card__desc">{{ card.description }}</p>
|
|
37
|
+
<div v-if="card.status === 'rate_limited'" class="card__rl">
|
|
38
|
+
<svg viewBox="0 0 12 12" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" aria-hidden="true"><circle cx="6" cy="6" r="4.6"></circle><path d="M6 3.6V6l1.8 1.2"></path></svg>
|
|
39
|
+
<span>rate limited · {{ rlText() }}</span>
|
|
40
|
+
</div>
|
|
32
41
|
<div class="card__foot">
|
|
33
42
|
<PriorityChip :priority="card.priority" />
|
|
34
43
|
<span class="chip chip--muted">{{ card.type }}</span>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox — ✅ EDITABLE. Editable dropdown: free-text input + curated
|
|
3
|
+
* suggestions (optionally grouped). Pure: modelValue in, update:modelValue out.
|
|
4
|
+
* `groups`: [{ label|null, options: string[] }] — label null renders flat.
|
|
5
|
+
* The user may pick a suggestion OR type any value.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { computed, onMounted, onUnmounted, ref } from "../vendor/vue.esm-browser.js";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: "Combobox",
|
|
12
|
+
props: {
|
|
13
|
+
modelValue: { type: String, default: "" },
|
|
14
|
+
groups: { type: Array, default: () => [] },
|
|
15
|
+
placeholder: { type: String, default: "" },
|
|
16
|
+
},
|
|
17
|
+
emits: ["update:modelValue"],
|
|
18
|
+
setup(props, { emit }) {
|
|
19
|
+
const open = ref(false);
|
|
20
|
+
const rootEl = ref(null);
|
|
21
|
+
const filtered = computed(() => {
|
|
22
|
+
const q = (props.modelValue || "").trim().toLowerCase();
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const g of props.groups) {
|
|
25
|
+
const opts = q ? g.options.filter((o) => o.toLowerCase().includes(q)) : g.options;
|
|
26
|
+
if (opts.length) out.push({ label: g.label || null, options: opts });
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
});
|
|
30
|
+
function onInput(e) {
|
|
31
|
+
emit("update:modelValue", e.target.value);
|
|
32
|
+
open.value = true;
|
|
33
|
+
}
|
|
34
|
+
function pick(o) {
|
|
35
|
+
emit("update:modelValue", o);
|
|
36
|
+
open.value = false;
|
|
37
|
+
}
|
|
38
|
+
function onDoc(e) {
|
|
39
|
+
if (rootEl.value && !rootEl.value.contains(e.target)) open.value = false;
|
|
40
|
+
}
|
|
41
|
+
function onKey(e) {
|
|
42
|
+
if (e.key === "Escape") open.value = false;
|
|
43
|
+
}
|
|
44
|
+
onMounted(() => {
|
|
45
|
+
document.addEventListener("click", onDoc, true);
|
|
46
|
+
document.addEventListener("keydown", onKey);
|
|
47
|
+
});
|
|
48
|
+
onUnmounted(() => {
|
|
49
|
+
document.removeEventListener("click", onDoc, true);
|
|
50
|
+
document.removeEventListener("keydown", onKey);
|
|
51
|
+
});
|
|
52
|
+
return { open, rootEl, filtered, onInput, pick };
|
|
53
|
+
},
|
|
54
|
+
template: `
|
|
55
|
+
<div class="dropdown combobox" ref="rootEl">
|
|
56
|
+
<div class="combobox__wrap">
|
|
57
|
+
<input class="input combobox__input" :value="modelValue" :placeholder="placeholder"
|
|
58
|
+
role="combobox" :aria-expanded="open" aria-autocomplete="list"
|
|
59
|
+
@input="onInput" @focus="open = true" />
|
|
60
|
+
<button type="button" class="combobox__toggle" :class="{ 'is-open': open }"
|
|
61
|
+
aria-label="Show suggestions" tabindex="-1" @click="open = !open">▾</button>
|
|
62
|
+
</div>
|
|
63
|
+
<ul v-if="open && filtered.length" class="dropdown__menu" role="listbox">
|
|
64
|
+
<template v-for="g in filtered" :key="g.label || '_'">
|
|
65
|
+
<li v-if="g.label" class="combobox__group" role="presentation">{{ g.label }}</li>
|
|
66
|
+
<li v-for="o in g.options" :key="o">
|
|
67
|
+
<button type="button" class="dropdown__opt" role="option"
|
|
68
|
+
:aria-selected="o === modelValue" :class="{ 'is-selected': o === modelValue }"
|
|
69
|
+
@click="pick(o)">
|
|
70
|
+
<span class="dropdown__optlabel">{{ o }}</span>
|
|
71
|
+
<span v-if="o === modelValue" class="dropdown__check">✓</span>
|
|
72
|
+
</button>
|
|
73
|
+
</li>
|
|
74
|
+
</template>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
`,
|
|
78
|
+
};
|
|
@@ -6,6 +6,9 @@ import { ref } from "../vendor/vue.esm-browser.js";
|
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
name: "CommentComposer",
|
|
9
|
+
props: {
|
|
10
|
+
busy: { type: Boolean, default: false },
|
|
11
|
+
},
|
|
9
12
|
emits: ["submit"],
|
|
10
13
|
setup(props, { emit }) {
|
|
11
14
|
const text = ref("");
|
|
@@ -20,7 +23,7 @@ export default {
|
|
|
20
23
|
template: `
|
|
21
24
|
<form class="composer" @submit.prevent="send">
|
|
22
25
|
<textarea class="input input--area" v-model="text" rows="2" placeholder="Add a comment…"></textarea>
|
|
23
|
-
<button class="btn btn--primary" type="submit" :disabled="!text.trim()">Comment</button>
|
|
26
|
+
<button class="btn btn--primary" type="submit" :disabled="!text.trim() || busy">{{ busy ? 'Sending…' : 'Comment' }}</button>
|
|
24
27
|
</form>
|
|
25
28
|
`,
|
|
26
29
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import Card from "./Card.js";
|
|
7
|
+
import { computed } from "../vendor/vue.esm-browser.js";
|
|
7
8
|
|
|
8
9
|
export default {
|
|
9
10
|
name: "Lane",
|
|
@@ -16,13 +17,17 @@ export default {
|
|
|
16
17
|
},
|
|
17
18
|
emits: ["open"],
|
|
18
19
|
setup(props, { emit }) {
|
|
19
|
-
|
|
20
|
+
const rateLimited = computed(() => props.cards.filter((c) => c.status === "rate_limited").length);
|
|
21
|
+
return { emit, rateLimited };
|
|
20
22
|
},
|
|
21
23
|
template: `
|
|
22
24
|
<section class="lane" :class="'lane--' + status" role="listitem">
|
|
23
25
|
<header class="lane__head">
|
|
24
26
|
<span class="lane__dot"></span>
|
|
25
27
|
<h2 class="lane__title">{{ title }}</h2>
|
|
28
|
+
<span v-if="rateLimited" class="lane__rl" :title="rateLimited + ' card(s) waiting on a provider rate limit'">
|
|
29
|
+
<svg viewBox="0 0 12 12" width="10" height="10" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" aria-hidden="true"><circle cx="6" cy="6" r="4.6"></circle><path d="M6 3.6V6l1.8 1.2"></path></svg>{{ rateLimited }}
|
|
30
|
+
</span>
|
|
26
31
|
<span class="lane__count">{{ cards.length }}</span>
|
|
27
32
|
</header>
|
|
28
33
|
<div class="lane__body">
|
|
@@ -7,6 +7,7 @@ export default {
|
|
|
7
7
|
props: {
|
|
8
8
|
loading: { type: Boolean, default: false },
|
|
9
9
|
error: { type: [String, null], default: null },
|
|
10
|
+
retryable: { type: Boolean, default: true },
|
|
10
11
|
},
|
|
11
12
|
emits: ["retry"],
|
|
12
13
|
setup(props, { emit }) {
|
|
@@ -14,9 +15,10 @@ export default {
|
|
|
14
15
|
},
|
|
15
16
|
template: `
|
|
16
17
|
<div v-if="error" class="notice notice--error">
|
|
17
|
-
<
|
|
18
|
-
<
|
|
18
|
+
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true" style="flex:none"><circle cx="8" cy="8" r="6.2"></circle><path d="M8 4.8V8.6M8 11.2v.1"></path></svg>
|
|
19
|
+
<span class="notice__msg">{{ error }}</span>
|
|
20
|
+
<button v-if="retryable" class="btn btn--ghost btn--sm" type="button" @click="emit('retry')">Retry</button>
|
|
19
21
|
</div>
|
|
20
|
-
<div v-else-if="loading" class="notice notice--info">Loading…</div>
|
|
22
|
+
<div v-else-if="loading" class="notice notice--info"><span class="spinner" aria-hidden="true"></span>Loading…</div>
|
|
21
23
|
`,
|
|
22
24
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rate_limited helpers — ✅ EDITABLE. Shared by Card tile, Lane badge and the
|
|
3
|
+
* card detail notice. Pure functions: card in, display strings out.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PROVIDER_LABELS = { claude: "Claude", copilot: "Copilot" };
|
|
7
|
+
|
|
8
|
+
/** Live countdown text derived from retryAt (null = unknown reset window). */
|
|
9
|
+
export function retryLabel(card, now = Date.now()) {
|
|
10
|
+
if (!card.retryAt) return "retrying when limit resets";
|
|
11
|
+
const mins = Math.ceil((new Date(card.retryAt).getTime() - now) / 60000);
|
|
12
|
+
if (mins <= 0) return "retrying\u2026";
|
|
13
|
+
if (mins >= 60) return `retry in ~${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
14
|
+
return `retry in ~${mins}m`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function providerLabel(card) {
|
|
18
|
+
return PROVIDER_LABELS[card.rateLimitProvider] || "";
|
|
19
|
+
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
const T0 = "2026-08-08T09:00:00.000Z";
|
|
13
13
|
const T1 = "2026-08-08T12:30:00.000Z";
|
|
14
14
|
const T2 = "2026-08-08T14:05:00.000Z";
|
|
15
|
+
/** rate_limited demo: auto-retry ~30 minutes from page load. */
|
|
16
|
+
const RETRY_AT = new Date(Date.now() + 30 * 60000).toISOString();
|
|
15
17
|
|
|
16
18
|
/** @param {Partial<import('../api/contract.js').BoardCard>} o */
|
|
17
19
|
function card(o) {
|
|
@@ -31,6 +33,8 @@ function card(o) {
|
|
|
31
33
|
nextSpawnAt: null,
|
|
32
34
|
workerThreadId: null,
|
|
33
35
|
blockedReason: null,
|
|
36
|
+
rateLimitProvider: null,
|
|
37
|
+
retryAt: null,
|
|
34
38
|
archivedAt: null,
|
|
35
39
|
createdAt: T0,
|
|
36
40
|
updatedAt: T0,
|
|
@@ -135,6 +139,36 @@ export const fixtures = {
|
|
|
135
139
|
archivedAt: T1,
|
|
136
140
|
profile: {},
|
|
137
141
|
}),
|
|
142
|
+
card({
|
|
143
|
+
cardId: 8,
|
|
144
|
+
title: "Refactor comment threading into a tree view",
|
|
145
|
+
description: "Worker paused mid-run: Claude usage limit reached. Auto-retries when the window resets.",
|
|
146
|
+
acceptance: "Nested replies render as a collapsible tree; existing flat view unaffected.",
|
|
147
|
+
type: "feature",
|
|
148
|
+
laneThreadId: 101,
|
|
149
|
+
priority: 1,
|
|
150
|
+
status: "rate_limited",
|
|
151
|
+
workerThreadId: 9902,
|
|
152
|
+
startedAt: T1,
|
|
153
|
+
rateLimitProvider: "claude",
|
|
154
|
+
retryAt: RETRY_AT,
|
|
155
|
+
profile: { agentType: "claude", createBranch: true, runTests: true },
|
|
156
|
+
}),
|
|
157
|
+
card({
|
|
158
|
+
cardId: 9,
|
|
159
|
+
title: "Generate API usage examples for the docs site",
|
|
160
|
+
description: "Copilot credits exhausted for this billing window; card is parked until they replenish.",
|
|
161
|
+
acceptance: "Each public endpoint has a runnable example snippet.",
|
|
162
|
+
type: "chore",
|
|
163
|
+
laneThreadId: 202,
|
|
164
|
+
priority: 0,
|
|
165
|
+
status: "rate_limited",
|
|
166
|
+
workerThreadId: 9903,
|
|
167
|
+
startedAt: T2,
|
|
168
|
+
rateLimitProvider: "copilot",
|
|
169
|
+
retryAt: null,
|
|
170
|
+
profile: { agentType: "copilot" },
|
|
171
|
+
}),
|
|
138
172
|
],
|
|
139
173
|
|
|
140
174
|
/** Keyed by cardId. */
|