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.
Files changed (236) hide show
  1. package/Install-Sensorium.ps1 +200 -49
  2. package/dist/board/components/Card.js +10 -1
  3. package/dist/board/components/Combobox.js +78 -0
  4. package/dist/board/components/CommentComposer.js +4 -1
  5. package/dist/board/components/Lane.js +6 -1
  6. package/dist/board/components/StateBanner.js +5 -3
  7. package/dist/board/components/StatusChip.js +1 -0
  8. package/dist/board/components/rateLimit.js +19 -0
  9. package/dist/board/mock/fixtures.js +34 -0
  10. package/dist/board/schema/policy.js +137 -72
  11. package/dist/board/styles/board.css +460 -3
  12. package/dist/board/views/BoardView.js +8 -2
  13. package/dist/board/views/BuilderView.js +334 -29
  14. package/dist/board/views/CardView.js +36 -17
  15. package/dist/config/agent-settings.d.ts +9 -0
  16. package/dist/config/agent-settings.d.ts.map +1 -1
  17. package/dist/config/agent-settings.js +56 -0
  18. package/dist/config/agent-settings.js.map +1 -1
  19. package/dist/config/detail-grant.d.ts +7 -16
  20. package/dist/config/detail-grant.d.ts.map +1 -1
  21. package/dist/config/detail-grant.js +12 -35
  22. package/dist/config/detail-grant.js.map +1 -1
  23. package/dist/config/index.d.ts +1 -0
  24. package/dist/config/index.d.ts.map +1 -1
  25. package/dist/config/index.js +1 -0
  26. package/dist/config/index.js.map +1 -1
  27. package/dist/config/misc-settings.d.ts +8 -1
  28. package/dist/config/misc-settings.d.ts.map +1 -1
  29. package/dist/config/misc-settings.js +40 -3
  30. package/dist/config/misc-settings.js.map +1 -1
  31. package/dist/config/operator-grant.d.ts +9 -0
  32. package/dist/config/operator-grant.d.ts.map +1 -0
  33. package/dist/config/operator-grant.js +24 -0
  34. package/dist/config/operator-grant.js.map +1 -0
  35. package/dist/config/voice-grant.d.ts +10 -0
  36. package/dist/config/voice-grant.d.ts.map +1 -0
  37. package/dist/config/voice-grant.js +18 -0
  38. package/dist/config/voice-grant.js.map +1 -0
  39. package/dist/dashboard/presets.d.ts +1 -1
  40. package/dist/dashboard/presets.d.ts.map +1 -1
  41. package/dist/dashboard/presets.js +1 -1
  42. package/dist/dashboard/presets.js.map +1 -1
  43. package/dist/dashboard/routes/board.d.ts +2 -0
  44. package/dist/dashboard/routes/board.d.ts.map +1 -1
  45. package/dist/dashboard/routes/board.js +171 -0
  46. package/dist/dashboard/routes/board.js.map +1 -1
  47. package/dist/dashboard/routes/claude.d.ts.map +1 -1
  48. package/dist/dashboard/routes/claude.js +1 -10
  49. package/dist/dashboard/routes/claude.js.map +1 -1
  50. package/dist/dashboard/routes/codex.d.ts +14 -0
  51. package/dist/dashboard/routes/codex.d.ts.map +1 -0
  52. package/dist/dashboard/routes/codex.js +92 -0
  53. package/dist/dashboard/routes/codex.js.map +1 -0
  54. package/dist/dashboard/routes/prompts.d.ts +2 -2
  55. package/dist/dashboard/routes/prompts.d.ts.map +1 -1
  56. package/dist/dashboard/routes/prompts.js +2 -2
  57. package/dist/dashboard/routes/prompts.js.map +1 -1
  58. package/dist/dashboard/routes/threads-read.d.ts +10 -7
  59. package/dist/dashboard/routes/threads-read.d.ts.map +1 -1
  60. package/dist/dashboard/routes/threads-read.js +21 -16
  61. package/dist/dashboard/routes/threads-read.js.map +1 -1
  62. package/dist/dashboard/routes.d.ts.map +1 -1
  63. package/dist/dashboard/routes.js +5 -1
  64. package/dist/dashboard/routes.js.map +1 -1
  65. package/dist/dashboard/spa.html +10 -10
  66. package/dist/data/memory/bootstrap.d.ts +1 -8
  67. package/dist/data/memory/bootstrap.d.ts.map +1 -1
  68. package/dist/data/memory/bootstrap.js +27 -37
  69. package/dist/data/memory/bootstrap.js.map +1 -1
  70. package/dist/data/memory/consolidation.d.ts +20 -1
  71. package/dist/data/memory/consolidation.d.ts.map +1 -1
  72. package/dist/data/memory/consolidation.js +53 -3
  73. package/dist/data/memory/consolidation.js.map +1 -1
  74. package/dist/data/memory/index.d.ts +0 -4
  75. package/dist/data/memory/index.d.ts.map +1 -1
  76. package/dist/data/memory/index.js +0 -4
  77. package/dist/data/memory/index.js.map +1 -1
  78. package/dist/data/memory/key-knowledge.d.ts +51 -0
  79. package/dist/data/memory/key-knowledge.d.ts.map +1 -0
  80. package/dist/data/memory/key-knowledge.js +128 -0
  81. package/dist/data/memory/key-knowledge.js.map +1 -0
  82. package/dist/data/memory/migration-runner.d.ts +1 -1
  83. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  84. package/dist/data/memory/migration-runner.js +19 -1
  85. package/dist/data/memory/migration-runner.js.map +1 -1
  86. package/dist/data/memory/narrative-lint.d.ts +2 -2
  87. package/dist/data/memory/narrative-lint.d.ts.map +1 -1
  88. package/dist/data/memory/narrative-lint.js +23 -13
  89. package/dist/data/memory/narrative-lint.js.map +1 -1
  90. package/dist/data/memory/narrative-resolutions.d.ts +9 -2
  91. package/dist/data/memory/narrative-resolutions.d.ts.map +1 -1
  92. package/dist/data/memory/narrative-resolutions.js +9 -8
  93. package/dist/data/memory/narrative-resolutions.js.map +1 -1
  94. package/dist/data/memory/narrative-sources.d.ts +1 -1
  95. package/dist/data/memory/narrative-sources.d.ts.map +1 -1
  96. package/dist/data/memory/narrative-sources.js +8 -3
  97. package/dist/data/memory/narrative-sources.js.map +1 -1
  98. package/dist/data/memory/narrative-store.d.ts +10 -0
  99. package/dist/data/memory/narrative-store.d.ts.map +1 -1
  100. package/dist/data/memory/narrative-store.js +37 -35
  101. package/dist/data/memory/narrative-store.js.map +1 -1
  102. package/dist/data/memory/narrative.d.ts.map +1 -1
  103. package/dist/data/memory/narrative.js +34 -28
  104. package/dist/data/memory/narrative.js.map +1 -1
  105. package/dist/data/memory/note-archival.d.ts +1 -1
  106. package/dist/data/memory/note-archival.d.ts.map +1 -1
  107. package/dist/data/memory/note-archival.js +7 -3
  108. package/dist/data/memory/note-archival.js.map +1 -1
  109. package/dist/data/memory/note-embeddings.d.ts +7 -0
  110. package/dist/data/memory/note-embeddings.d.ts.map +1 -1
  111. package/dist/data/memory/note-embeddings.js +6 -2
  112. package/dist/data/memory/note-embeddings.js.map +1 -1
  113. package/dist/data/memory/schema-ddl.d.ts +1 -1
  114. package/dist/data/memory/schema-ddl.d.ts.map +1 -1
  115. package/dist/data/memory/schema-ddl.js +3 -1
  116. package/dist/data/memory/schema-ddl.js.map +1 -1
  117. package/dist/data/memory/semantic-notes.d.ts +13 -8
  118. package/dist/data/memory/semantic-notes.d.ts.map +1 -1
  119. package/dist/data/memory/semantic-notes.js +12 -17
  120. package/dist/data/memory/semantic-notes.js.map +1 -1
  121. package/dist/data/memory/thread-registry.d.ts +7 -1
  122. package/dist/data/memory/thread-registry.d.ts.map +1 -1
  123. package/dist/data/memory/thread-registry.js +14 -5
  124. package/dist/data/memory/thread-registry.js.map +1 -1
  125. package/dist/data/memory/utils.d.ts +6 -0
  126. package/dist/data/memory/utils.d.ts.map +1 -1
  127. package/dist/data/memory/utils.js +13 -0
  128. package/dist/data/memory/utils.js.map +1 -1
  129. package/dist/integrations/telegram/types.d.ts +11 -0
  130. package/dist/integrations/telegram/types.d.ts.map +1 -1
  131. package/dist/llm/prompts/narrative.d.ts.map +1 -1
  132. package/dist/llm/prompts/narrative.js +27 -86
  133. package/dist/llm/prompts/narrative.js.map +1 -1
  134. package/dist/services/claude-spawn.service.d.ts.map +1 -1
  135. package/dist/services/claude-spawn.service.js +6 -1
  136. package/dist/services/claude-spawn.service.js.map +1 -1
  137. package/dist/services/codex-spawn.service.d.ts.map +1 -1
  138. package/dist/services/codex-spawn.service.js +10 -4
  139. package/dist/services/codex-spawn.service.js.map +1 -1
  140. package/dist/services/consolidation.service.d.ts.map +1 -1
  141. package/dist/services/consolidation.service.js +10 -43
  142. package/dist/services/consolidation.service.js.map +1 -1
  143. package/dist/services/copilot-spawn.service.d.ts.map +1 -1
  144. package/dist/services/copilot-spawn.service.js +6 -1
  145. package/dist/services/copilot-spawn.service.js.map +1 -1
  146. package/dist/services/daily-session.d.ts +22 -11
  147. package/dist/services/daily-session.d.ts.map +1 -1
  148. package/dist/services/daily-session.js +69 -24
  149. package/dist/services/daily-session.js.map +1 -1
  150. package/dist/services/dispatcher/broker.d.ts +9 -0
  151. package/dist/services/dispatcher/broker.d.ts.map +1 -1
  152. package/dist/services/dispatcher/broker.js.map +1 -1
  153. package/dist/services/dispatcher/poller.d.ts.map +1 -1
  154. package/dist/services/dispatcher/poller.js +31 -24
  155. package/dist/services/dispatcher/poller.js.map +1 -1
  156. package/dist/services/mcp-config.service.d.ts.map +1 -1
  157. package/dist/services/mcp-config.service.js +10 -1
  158. package/dist/services/mcp-config.service.js.map +1 -1
  159. package/dist/services/memory-briefing.service.d.ts +1 -1
  160. package/dist/services/memory-briefing.service.d.ts.map +1 -1
  161. package/dist/services/memory-briefing.service.js +28 -73
  162. package/dist/services/memory-briefing.service.js.map +1 -1
  163. package/dist/services/memory-pruning.service.d.ts.map +1 -1
  164. package/dist/services/memory-pruning.service.js +22 -4
  165. package/dist/services/memory-pruning.service.js.map +1 -1
  166. package/dist/services/predefined-prompts.d.ts +11 -0
  167. package/dist/services/predefined-prompts.d.ts.map +1 -0
  168. package/dist/services/predefined-prompts.js +25 -0
  169. package/dist/services/predefined-prompts.js.map +1 -0
  170. package/dist/services/spawn-common.d.ts +10 -2
  171. package/dist/services/spawn-common.d.ts.map +1 -1
  172. package/dist/services/spawn-common.js +14 -4
  173. package/dist/services/spawn-common.js.map +1 -1
  174. package/dist/sessions/mcp-registry.d.ts +7 -2
  175. package/dist/sessions/mcp-registry.d.ts.map +1 -1
  176. package/dist/sessions/mcp-registry.js +52 -30
  177. package/dist/sessions/mcp-registry.js.map +1 -1
  178. package/dist/tools/defs/memory-defs.d.ts.map +1 -1
  179. package/dist/tools/defs/memory-defs.js +12 -4
  180. package/dist/tools/defs/memory-defs.js.map +1 -1
  181. package/dist/tools/defs/session-defs.js +2 -2
  182. package/dist/tools/defs/session-defs.js.map +1 -1
  183. package/dist/tools/defs/utility-defs.js +1 -1
  184. package/dist/tools/defs/utility-defs.js.map +1 -1
  185. package/dist/tools/memory-tools.d.ts.map +1 -1
  186. package/dist/tools/memory-tools.js +66 -9
  187. package/dist/tools/memory-tools.js.map +1 -1
  188. package/dist/tools/session-tools.js +3 -10
  189. package/dist/tools/session-tools.js.map +1 -1
  190. package/dist/tools/start-session-tool.d.ts.map +1 -1
  191. package/dist/tools/start-session-tool.js +13 -1
  192. package/dist/tools/start-session-tool.js.map +1 -1
  193. package/dist/tools/utility-tools.d.ts.map +1 -1
  194. package/dist/tools/utility-tools.js +31 -0
  195. package/dist/tools/utility-tools.js.map +1 -1
  196. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  197. package/dist/tools/wait/drive-handler.js +18 -14
  198. package/dist/tools/wait/drive-handler.js.map +1 -1
  199. package/dist/tools/wait/message-delivery.d.ts +9 -6
  200. package/dist/tools/wait/message-delivery.d.ts.map +1 -1
  201. package/dist/tools/wait/message-delivery.js +71 -65
  202. package/dist/tools/wait/message-delivery.js.map +1 -1
  203. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  204. package/dist/tools/wait/message-processing.js +2 -1
  205. package/dist/tools/wait/message-processing.js.map +1 -1
  206. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  207. package/dist/tools/wait/poll-loop.js +6 -0
  208. package/dist/tools/wait/poll-loop.js.map +1 -1
  209. package/package.json +1 -1
  210. package/templates/increment-pr-pipeline.default.md +79 -0
  211. package/dist/data/memory/reflection-dedup.d.ts +0 -17
  212. package/dist/data/memory/reflection-dedup.d.ts.map +0 -1
  213. package/dist/data/memory/reflection-dedup.js +0 -55
  214. package/dist/data/memory/reflection-dedup.js.map +0 -1
  215. package/dist/data/memory/reflection-gates.d.ts +0 -40
  216. package/dist/data/memory/reflection-gates.d.ts.map +0 -1
  217. package/dist/data/memory/reflection-gates.js +0 -220
  218. package/dist/data/memory/reflection-gates.js.map +0 -1
  219. package/dist/data/memory/reflection-sources.d.ts +0 -13
  220. package/dist/data/memory/reflection-sources.d.ts.map +0 -1
  221. package/dist/data/memory/reflection-sources.js +0 -115
  222. package/dist/data/memory/reflection-sources.js.map +0 -1
  223. package/dist/data/memory/reflection.d.ts +0 -43
  224. package/dist/data/memory/reflection.d.ts.map +0 -1
  225. package/dist/data/memory/reflection.js +0 -329
  226. package/dist/data/memory/reflection.js.map +0 -1
  227. package/dist/llm/prompts/reflection.d.ts +0 -6
  228. package/dist/llm/prompts/reflection.d.ts.map +0 -1
  229. package/dist/llm/prompts/reflection.js +0 -77
  230. package/dist/llm/prompts/reflection.js.map +0 -1
  231. package/templates/atomic-build-spec.default.md +0 -57
  232. package/templates/daily-review.default.md +0 -26
  233. package/templates/fire-and-forget.default.md +0 -76
  234. package/templates/offload.default.md +0 -33
  235. package/templates/phased-build.default.md +0 -59
  236. package/templates/sensorium-repair.default.md +0 -114
@@ -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
- $metadata | ConvertTo-Json | Set-Content -LiteralPath $MetadataPath -Encoding ASCII
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
- $tmpFile = "$Destination.tmp"
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
- # Atomic replace
260
- if (Test-Path $Destination) {
261
- $backupPath = "$Destination.bak"
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). Ensure profile '$SecureVaultProfile' maps a GitHub PAT to env var SENSORIUM_GH_TOKEN."
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
- $alreadyExists = Test-Path $Binary
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
- Write-Host ""
457
- if ($alreadyExists) {
458
- Write-Host "=== Sensorium Supervisor UPDATE ===" -ForegroundColor Cyan
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
- # Step 2 — ensure bin dir exists
476
- New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
600
+ $alreadyExists = Test-Path $Binary
477
601
 
478
- # Step 3 — fetch release metadata and download only when needed
479
- Sync-ReleaseBinary
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 3b — prepare the isolated MCP runtime area (opt-in): a private npm cache +
482
- # repo-independent cwd. The `npx ...@latest` command is unchanged (auto-update
483
- # preserved). On any failure this disables the isolation env in Start-Supervisor,
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 4 — install launcher and start
493
- Install-StartupLauncher
494
- Start-Supervisor
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 5 — verify
497
- Show-Status
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
- return { emit, elapsed };
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
- return { emit };
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
- <span>{{ error }}</span>
18
- <button class="btn btn--ghost btn--sm" type="button" @click="emit('retry')">Retry</button>
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
  };
@@ -9,6 +9,7 @@ const LABELS = {
9
9
  decision_needed: "Decision needed",
10
10
  blocked: "Blocked",
11
11
  done: "Done",
12
+ rate_limited: "Rate limited",
12
13
  };
13
14
 
14
15
  export default {
@@ -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. */