sensorium-mcp 2.17.27 → 3.0.0

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 (224) hide show
  1. package/Install-Sensorium.ps1 +327 -0
  2. package/README.md +14 -0
  3. package/dist/config.d.ts +16 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +39 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/daily-session.d.ts +2 -1
  8. package/dist/daily-session.d.ts.map +1 -1
  9. package/dist/daily-session.js +23 -26
  10. package/dist/daily-session.js.map +1 -1
  11. package/dist/dashboard/routes/settings.d.ts +4 -0
  12. package/dist/dashboard/routes/settings.d.ts.map +1 -1
  13. package/dist/dashboard/routes/settings.js +57 -1
  14. package/dist/dashboard/routes/settings.js.map +1 -1
  15. package/dist/dashboard/routes/threads.d.ts +1 -0
  16. package/dist/dashboard/routes/threads.d.ts.map +1 -1
  17. package/dist/dashboard/routes/threads.js +23 -27
  18. package/dist/dashboard/routes/threads.js.map +1 -1
  19. package/dist/dashboard/routes.d.ts.map +1 -1
  20. package/dist/dashboard/routes.js +7 -2
  21. package/dist/dashboard/routes.js.map +1 -1
  22. package/dist/dashboard/spa.html +11 -11
  23. package/dist/data/interfaces.d.ts +36 -0
  24. package/dist/data/interfaces.d.ts.map +1 -0
  25. package/dist/data/interfaces.js +2 -0
  26. package/dist/data/interfaces.js.map +1 -0
  27. package/dist/data/memory/bootstrap.d.ts +36 -16
  28. package/dist/data/memory/bootstrap.d.ts.map +1 -1
  29. package/dist/data/memory/bootstrap.js +71 -217
  30. package/dist/data/memory/bootstrap.js.map +1 -1
  31. package/dist/data/memory/consolidation.d.ts +35 -34
  32. package/dist/data/memory/consolidation.d.ts.map +1 -1
  33. package/dist/data/memory/consolidation.js +43 -555
  34. package/dist/data/memory/consolidation.js.map +1 -1
  35. package/dist/data/memory/index.d.ts +0 -1
  36. package/dist/data/memory/index.d.ts.map +1 -1
  37. package/dist/data/memory/index.js +0 -1
  38. package/dist/data/memory/index.js.map +1 -1
  39. package/dist/data/memory/migration-runner.d.ts +5 -0
  40. package/dist/data/memory/migration-runner.d.ts.map +1 -0
  41. package/dist/data/memory/migration-runner.js +403 -0
  42. package/dist/data/memory/migration-runner.js.map +1 -0
  43. package/dist/data/memory/reflection.js +1 -1
  44. package/dist/data/memory/schema-ddl.d.ts +4 -0
  45. package/dist/data/memory/schema-ddl.d.ts.map +1 -0
  46. package/dist/data/memory/schema-ddl.js +194 -0
  47. package/dist/data/memory/schema-ddl.js.map +1 -0
  48. package/dist/data/memory/schema-guard.d.ts +3 -0
  49. package/dist/data/memory/schema-guard.d.ts.map +1 -0
  50. package/dist/data/memory/schema-guard.js +184 -0
  51. package/dist/data/memory/schema-guard.js.map +1 -0
  52. package/dist/data/memory/schema.d.ts +2 -5
  53. package/dist/data/memory/schema.d.ts.map +1 -1
  54. package/dist/data/memory/schema.js +6 -834
  55. package/dist/data/memory/schema.js.map +1 -1
  56. package/dist/data/memory/semantic.d.ts +0 -1
  57. package/dist/data/memory/semantic.d.ts.map +1 -1
  58. package/dist/data/memory/semantic.js +2 -8
  59. package/dist/data/memory/semantic.js.map +1 -1
  60. package/dist/data/memory/synthesis.js +2 -2
  61. package/dist/data/memory/synthesis.js.map +1 -1
  62. package/dist/data/memory/thread-registry.d.ts +18 -4
  63. package/dist/data/memory/thread-registry.d.ts.map +1 -1
  64. package/dist/data/memory/thread-registry.js +25 -0
  65. package/dist/data/memory/thread-registry.js.map +1 -1
  66. package/dist/data/sent-message.repository.d.ts +12 -0
  67. package/dist/data/sent-message.repository.d.ts.map +1 -0
  68. package/dist/data/sent-message.repository.js +31 -0
  69. package/dist/data/sent-message.repository.js.map +1 -0
  70. package/dist/http-server.d.ts.map +1 -1
  71. package/dist/http-server.js +23 -2
  72. package/dist/http-server.js.map +1 -1
  73. package/dist/index.js +27 -48
  74. package/dist/index.js.map +1 -1
  75. package/dist/logger.d.ts +7 -2
  76. package/dist/logger.d.ts.map +1 -1
  77. package/dist/logger.js +89 -12
  78. package/dist/logger.js.map +1 -1
  79. package/dist/scheduler.d.ts +8 -0
  80. package/dist/scheduler.d.ts.map +1 -1
  81. package/dist/scheduler.js +15 -0
  82. package/dist/scheduler.js.map +1 -1
  83. package/dist/server/factory.d.ts +2 -1
  84. package/dist/server/factory.d.ts.map +1 -1
  85. package/dist/server/factory.js +11 -4
  86. package/dist/server/factory.js.map +1 -1
  87. package/dist/services/agent-spawn.service.d.ts +39 -0
  88. package/dist/services/agent-spawn.service.d.ts.map +1 -0
  89. package/dist/services/agent-spawn.service.js +348 -0
  90. package/dist/services/agent-spawn.service.js.map +1 -0
  91. package/dist/services/background-runner.d.ts +26 -0
  92. package/dist/services/background-runner.d.ts.map +1 -0
  93. package/dist/services/background-runner.js +71 -0
  94. package/dist/services/background-runner.js.map +1 -0
  95. package/dist/services/consolidation.service.d.ts +16 -0
  96. package/dist/services/consolidation.service.d.ts.map +1 -0
  97. package/dist/services/consolidation.service.js +508 -0
  98. package/dist/services/consolidation.service.js.map +1 -0
  99. package/dist/services/dispatcher/broker.d.ts +2 -0
  100. package/dist/services/dispatcher/broker.d.ts.map +1 -1
  101. package/dist/services/dispatcher/broker.js +5 -10
  102. package/dist/services/dispatcher/broker.js.map +1 -1
  103. package/dist/services/dispatcher/index.d.ts +1 -1
  104. package/dist/services/dispatcher/index.d.ts.map +1 -1
  105. package/dist/services/dispatcher/index.js +1 -1
  106. package/dist/services/dispatcher/index.js.map +1 -1
  107. package/dist/services/dispatcher/lock.d.ts.map +1 -1
  108. package/dist/services/dispatcher/lock.js +7 -11
  109. package/dist/services/dispatcher/lock.js.map +1 -1
  110. package/dist/services/maintenance-signal.d.ts +18 -0
  111. package/dist/services/maintenance-signal.d.ts.map +1 -0
  112. package/dist/services/maintenance-signal.js +48 -0
  113. package/dist/services/maintenance-signal.js.map +1 -0
  114. package/dist/services/memory-briefing.service.d.ts +4 -0
  115. package/dist/services/memory-briefing.service.d.ts.map +1 -0
  116. package/dist/services/memory-briefing.service.js +143 -0
  117. package/dist/services/memory-briefing.service.js.map +1 -0
  118. package/dist/services/process.service.d.ts +31 -0
  119. package/dist/services/process.service.d.ts.map +1 -0
  120. package/dist/services/process.service.js +100 -0
  121. package/dist/services/process.service.js.map +1 -0
  122. package/dist/services/thread-health.service.d.ts +18 -0
  123. package/dist/services/thread-health.service.d.ts.map +1 -0
  124. package/dist/services/thread-health.service.js +118 -0
  125. package/dist/services/thread-health.service.js.map +1 -0
  126. package/dist/services/thread-lifecycle.service.d.ts +52 -0
  127. package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
  128. package/dist/services/thread-lifecycle.service.js +174 -0
  129. package/dist/services/thread-lifecycle.service.js.map +1 -0
  130. package/dist/services/topic.service.d.ts +25 -0
  131. package/dist/services/topic.service.d.ts.map +1 -0
  132. package/dist/services/topic.service.js +65 -0
  133. package/dist/services/topic.service.js.map +1 -0
  134. package/dist/services/worker-cleanup.service.d.ts +8 -0
  135. package/dist/services/worker-cleanup.service.d.ts.map +1 -0
  136. package/dist/services/worker-cleanup.service.js +82 -0
  137. package/dist/services/worker-cleanup.service.js.map +1 -0
  138. package/dist/sessions.d.ts +14 -0
  139. package/dist/sessions.d.ts.map +1 -1
  140. package/dist/sessions.js +55 -0
  141. package/dist/sessions.js.map +1 -1
  142. package/dist/telegram.d.ts +13 -6
  143. package/dist/telegram.d.ts.map +1 -1
  144. package/dist/telegram.js +43 -14
  145. package/dist/telegram.js.map +1 -1
  146. package/dist/tools/defs/memory-defs.d.ts.map +1 -1
  147. package/dist/tools/defs/memory-defs.js +0 -19
  148. package/dist/tools/defs/memory-defs.js.map +1 -1
  149. package/dist/tools/delegate-tool.d.ts +4 -0
  150. package/dist/tools/delegate-tool.d.ts.map +1 -1
  151. package/dist/tools/delegate-tool.js +48 -109
  152. package/dist/tools/delegate-tool.js.map +1 -1
  153. package/dist/tools/memory-tools.d.ts.map +1 -1
  154. package/dist/tools/memory-tools.js +1 -16
  155. package/dist/tools/memory-tools.js.map +1 -1
  156. package/dist/tools/shared-agent-utils.d.ts +9 -1
  157. package/dist/tools/shared-agent-utils.d.ts.map +1 -1
  158. package/dist/tools/shared-agent-utils.js +24 -42
  159. package/dist/tools/shared-agent-utils.js.map +1 -1
  160. package/dist/tools/start-session-tool.d.ts +2 -0
  161. package/dist/tools/start-session-tool.d.ts.map +1 -1
  162. package/dist/tools/start-session-tool.js +66 -106
  163. package/dist/tools/start-session-tool.js.map +1 -1
  164. package/dist/tools/thread-lifecycle.d.ts +5 -127
  165. package/dist/tools/thread-lifecycle.d.ts.map +1 -1
  166. package/dist/tools/thread-lifecycle.js +5 -1163
  167. package/dist/tools/thread-lifecycle.js.map +1 -1
  168. package/dist/tools/utility-tools.js +5 -2
  169. package/dist/tools/utility-tools.js.map +1 -1
  170. package/dist/tools/wait/drive-handler.d.ts +0 -1
  171. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  172. package/dist/tools/wait/drive-handler.js +5 -22
  173. package/dist/tools/wait/drive-handler.js.map +1 -1
  174. package/dist/tools/wait/message-delivery.js +1 -1
  175. package/dist/tools/wait/message-delivery.js.map +1 -1
  176. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  177. package/dist/tools/wait/message-processing.js +9 -8
  178. package/dist/tools/wait/message-processing.js.map +1 -1
  179. package/dist/tools/wait/poll-loop.d.ts +2 -0
  180. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  181. package/dist/tools/wait/poll-loop.js +27 -29
  182. package/dist/tools/wait/poll-loop.js.map +1 -1
  183. package/dist/tools/wait/task-handler.d.ts +0 -3
  184. package/dist/tools/wait/task-handler.d.ts.map +1 -1
  185. package/dist/tools/wait/task-handler.js +3 -2
  186. package/dist/tools/wait/task-handler.js.map +1 -1
  187. package/dist/types.d.ts +0 -1
  188. package/dist/types.d.ts.map +1 -1
  189. package/package.json +4 -8
  190. package/supervisor/config.go +182 -69
  191. package/supervisor/config_test.go +78 -0
  192. package/supervisor/go.mod +12 -0
  193. package/supervisor/go.sum +20 -0
  194. package/supervisor/health.go +60 -11
  195. package/supervisor/health_test.go +29 -0
  196. package/supervisor/keeper.go +15 -10
  197. package/supervisor/log.go +109 -28
  198. package/supervisor/log_test.go +86 -6
  199. package/supervisor/main.go +150 -19
  200. package/supervisor/main_test.go +130 -0
  201. package/supervisor/process.go +47 -4
  202. package/supervisor/process_test.go +14 -0
  203. package/supervisor/secrets.go +95 -0
  204. package/supervisor/secrets_securevault_test.go +98 -0
  205. package/supervisor/secrets_test.go +119 -0
  206. package/supervisor/self_update.go +282 -0
  207. package/supervisor/self_update_test.go +177 -0
  208. package/supervisor/service_restart_stub.go +9 -0
  209. package/supervisor/service_restart_windows.go +63 -0
  210. package/supervisor/service_stub.go +15 -0
  211. package/supervisor/service_windows.go +216 -0
  212. package/supervisor/update_state.go +264 -0
  213. package/supervisor/update_state_test.go +306 -0
  214. package/supervisor/updater.go +311 -10
  215. package/supervisor/updater_test.go +64 -0
  216. package/dist/data/memory/quality-scoring.d.ts +0 -32
  217. package/dist/data/memory/quality-scoring.d.ts.map +0 -1
  218. package/dist/data/memory/quality-scoring.js +0 -182
  219. package/dist/data/memory/quality-scoring.js.map +0 -1
  220. package/scripts/install-supervisor.ps1 +0 -67
  221. package/scripts/install-supervisor.sh +0 -43
  222. package/scripts/start-supervisor.ps1 +0 -46
  223. package/scripts/start-supervisor.sh +0 -20
  224. package/templates/coding-task.default.md +0 -12
@@ -4,21 +4,31 @@ import (
4
4
  "context"
5
5
  "encoding/json"
6
6
  "fmt"
7
+ "io"
7
8
  "net/http"
8
9
  "os"
9
10
  "path/filepath"
10
11
  "runtime"
11
12
  "strings"
13
+ "syscall"
12
14
  "time"
13
15
  )
14
16
 
15
17
  const registryURL = "https://registry.npmjs.org/sensorium-mcp/latest"
18
+ const supervisorReleaseURL = "https://api.github.com/repos/andriyshevchenko/remote-copilot-mcp/releases/tags/supervisor-latest"
19
+
20
+ var (
21
+ notifyUpdaterOperator = NotifyOperator
22
+ mcpUpdateReadyPollInterval = 3 * time.Second
23
+ mcpUpdateReadyTimeout = 60 * time.Second
24
+ )
16
25
 
17
26
  // Updater checks the npm registry for new versions and performs updates.
18
27
  type Updater struct {
19
28
  cfg Config
20
29
  mcp *MCPClient
21
30
  log *Logger
31
+ state *UpdateStateStore
22
32
  startAt time.Time
23
33
  cancel context.CancelFunc
24
34
  done chan struct{}
@@ -29,6 +39,7 @@ func NewUpdater(cfg Config, mcp *MCPClient, log *Logger) *Updater {
29
39
  cfg: cfg,
30
40
  mcp: mcp,
31
41
  log: log,
42
+ state: NewUpdateStateStore(cfg.Paths.UpdateState, log),
32
43
  startAt: time.Now(),
33
44
  done: make(chan struct{}),
34
45
  }
@@ -75,6 +86,10 @@ func (u *Updater) run(ctx context.Context) {
75
86
  }
76
87
 
77
88
  u.checkAndUpdate(ctx)
89
+ if ctx.Err() != nil {
90
+ return
91
+ }
92
+ u.checkSupervisorUpdate(ctx)
78
93
  }
79
94
  }
80
95
 
@@ -159,30 +174,54 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
159
174
  }
160
175
 
161
176
  u.log.Info("Update available: %s → %s", local, remote)
162
- NotifyOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating sensorium v%s → v%s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
177
+ coordLock, ok := AcquireUpdateCoordinatorLock(u.cfg.Paths.UpdateApplyLock, updateScopeMCP, u.log)
178
+ if !ok {
179
+ u.log.Info("Deferring MCP update %s → %s due to active update apply lock", local, remote)
180
+ return
181
+ }
182
+ defer coordLock.Release()
183
+
184
+ u.state.Transition(updateScopeMCP, updatePhaseApplying, remote, local, "")
185
+ markFailed := func(err error) {
186
+ u.state.Transition(updateScopeMCP, updatePhaseFailed, remote, local, err.Error())
187
+ }
188
+
189
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating sensorium v%s → v%s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
163
190
 
164
191
  // Grace period
165
192
  u.log.Info("Grace period %v...", u.cfg.GracePeriod)
166
193
  select {
167
194
  case <-ctx.Done():
195
+ markFailed(ctx.Err())
168
196
  return
169
197
  case <-time.After(u.cfg.GracePeriod):
170
198
  }
171
199
 
172
- // Set maintenance flag — always clean up on exit
173
- if err := atomicWrite(u.cfg.Paths.MaintenanceFlag, []byte(time.Now().Format(time.RFC3339))); err != nil {
200
+ // Set maintenance flag — always clean up on exit.
201
+ // Written as JSON so TypeScript's checkMaintenanceFlag() can parse the
202
+ // version and timestamp fields for accurate maintenance notifications.
203
+ maintenanceJSON, err := json.Marshal(map[string]string{
204
+ "version": remote,
205
+ "timestamp": time.Now().Format(time.RFC3339),
206
+ })
207
+ if err != nil {
208
+ u.log.Warn("Failed to marshal maintenance flag: %v", err)
209
+ } else if err := atomicWrite(u.cfg.Paths.MaintenanceFlag, maintenanceJSON); err != nil {
174
210
  u.log.Warn("Failed to write maintenance flag: %v", err)
175
211
  }
176
212
  defer os.Remove(u.cfg.Paths.MaintenanceFlag)
177
213
 
178
214
  // Kill the current MCP server
179
215
  if ctx.Err() != nil {
216
+ markFailed(ctx.Err())
180
217
  return
181
218
  }
219
+ u.state.Transition(updateScopeMCP, updatePhaseRestarting, remote, local, "")
182
220
  u.killServer()
183
221
 
184
222
  // Clean npx cache
185
223
  if ctx.Err() != nil {
224
+ markFailed(ctx.Err())
186
225
  return
187
226
  }
188
227
  u.clearNpxCache()
@@ -191,6 +230,7 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
191
230
  var pid int
192
231
  for attempt := 1; attempt <= 3; attempt++ {
193
232
  if ctx.Err() != nil {
233
+ markFailed(ctx.Err())
194
234
  return
195
235
  }
196
236
  pid, err = SpawnMCPServer(u.cfg, u.log)
@@ -204,26 +244,287 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
204
244
  }
205
245
  if err != nil {
206
246
  u.log.Error("All spawn attempts failed — server is down!")
207
- NotifyOperator(u.cfg, u.log, "🔴 Supervisor: update FAILED — server is down! Manual intervention required.", 0)
247
+ markFailed(err)
248
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update FAILED — server is down! Manual intervention required.", 0)
208
249
  return
209
250
  }
210
251
 
211
- // Wait for new server to be ready
212
- if u.mcp.WaitForReady(ctx, 3*time.Second, 60*time.Second) {
213
- u.log.Info("Updated MCP server ready (PID %d)", pid)
214
- } else {
215
- u.log.Warn("Updated server did not become ready in 60s")
252
+ if !u.verifyUpdatedMCPServerReady(ctx, remote, local, pid) {
253
+ return
216
254
  }
217
255
 
218
256
  u.setLocalVersion(remote)
257
+ u.state.Transition(updateScopeMCP, updatePhaseIdle, remote, local, "")
219
258
 
220
- NotifyOperator(u.cfg, u.log, fmt.Sprintf("✅ Supervisor: update to v%s complete. Server ready.", remote), 0)
259
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("✅ Supervisor: update to v%s complete. Server ready.", remote), 0)
221
260
  u.log.Info("Update complete: v%s → v%s", local, remote)
222
261
 
223
262
  // Reset start time for min uptime tracking
224
263
  u.startAt = time.Now()
225
264
  }
226
265
 
266
+ func (u *Updater) verifyUpdatedMCPServerReady(ctx context.Context, remote, local string, pid int) bool {
267
+ u.state.Transition(updateScopeMCP, updatePhaseVerifying, remote, local, "")
268
+ if u.mcp.WaitForReady(ctx, mcpUpdateReadyPollInterval, mcpUpdateReadyTimeout) {
269
+ u.log.Info("Updated MCP server ready (PID %d)", pid)
270
+ return true
271
+ }
272
+
273
+ errMsg := fmt.Sprintf("updated MCP server did not become ready within %v after restart (pid=%d)", mcpUpdateReadyTimeout, pid)
274
+ u.log.Error(errMsg)
275
+ u.state.Transition(updateScopeMCP, updatePhaseFailed, remote, local, errMsg)
276
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: update to v%s FAILED verification. Server did not become ready after restart.", remote), 0)
277
+ return false
278
+ }
279
+
280
+ type githubRelease struct {
281
+ TagName string `json:"tag_name"`
282
+ Name string `json:"name"`
283
+ Assets []struct {
284
+ Name string `json:"name"`
285
+ URL string `json:"browser_download_url"`
286
+ Size int64 `json:"size"`
287
+ } `json:"assets"`
288
+ }
289
+
290
+ func (u *Updater) getSupervisorRelease(ctx context.Context) (string, string, error) {
291
+ ctx2, cancel := context.WithTimeout(ctx, 20*time.Second)
292
+ defer cancel()
293
+
294
+ req, err := http.NewRequestWithContext(ctx2, http.MethodGet, supervisorReleaseURL, nil)
295
+ if err != nil {
296
+ return "", "", err
297
+ }
298
+ req.Header.Set("Accept", "application/vnd.github+json")
299
+ req.Header.Set("User-Agent", "sensorium-supervisor-updater")
300
+
301
+ resp, err := http.DefaultClient.Do(req)
302
+ if err != nil {
303
+ return "", "", err
304
+ }
305
+ defer resp.Body.Close()
306
+
307
+ if resp.StatusCode != http.StatusOK {
308
+ return "", "", fmt.Errorf("GitHub releases HTTP %d", resp.StatusCode)
309
+ }
310
+
311
+ var release githubRelease
312
+ if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
313
+ return "", "", err
314
+ }
315
+
316
+ assetName := supervisorAssetName()
317
+ for _, asset := range release.Assets {
318
+ if asset.Name != assetName {
319
+ continue
320
+ }
321
+
322
+ version := strings.TrimSpace(release.Name)
323
+ if version == "" {
324
+ version = strings.TrimSpace(release.TagName)
325
+ }
326
+ if version == "" {
327
+ return "", "", fmt.Errorf("release version missing for %s", assetName)
328
+ }
329
+ if strings.TrimSpace(asset.URL) == "" {
330
+ return "", "", fmt.Errorf("release asset URL missing for %s", assetName)
331
+ }
332
+
333
+ return version, asset.URL, nil
334
+ }
335
+
336
+ return "", "", fmt.Errorf("release asset %q not found", assetName)
337
+ }
338
+
339
+ func supervisorAssetName() string {
340
+ suffix := ""
341
+ if runtime.GOOS == "windows" {
342
+ suffix = ".exe"
343
+ }
344
+ return fmt.Sprintf("sensorium-supervisor-%s-%s%s", runtime.GOOS, runtime.GOARCH, suffix)
345
+ }
346
+
347
+ func (u *Updater) getLocalSupervisorVersion() string {
348
+ data, err := os.ReadFile(u.cfg.Paths.SupervisorVersion)
349
+ if err != nil {
350
+ return ""
351
+ }
352
+ return strings.TrimSpace(string(data))
353
+ }
354
+
355
+ func (u *Updater) setLocalSupervisorVersion(v string) {
356
+ os.MkdirAll(u.cfg.DataDir, 0755)
357
+ if err := atomicWrite(u.cfg.Paths.SupervisorVersion, []byte(v)); err != nil {
358
+ u.log.Warn("Failed to write supervisor version file: %v", err)
359
+ }
360
+ }
361
+
362
+ func (u *Updater) stagePendingSupervisorVersion(v string) error {
363
+ if err := os.MkdirAll(filepath.Dir(u.cfg.Paths.PendingVersion), 0755); err != nil {
364
+ return fmt.Errorf("create pending supervisor version dir: %w", err)
365
+ }
366
+ if err := atomicWrite(u.cfg.Paths.PendingVersion, []byte(v)); err != nil {
367
+ return fmt.Errorf("write pending supervisor version: %w", err)
368
+ }
369
+ return nil
370
+ }
371
+
372
+ func (u *Updater) checkSupervisorUpdate(ctx context.Context) {
373
+ uptime := time.Since(u.startAt)
374
+ if uptime < u.cfg.MinUptime {
375
+ u.log.Info("Deferring supervisor update — too early (uptime %v < %v)", uptime.Round(time.Second), u.cfg.MinUptime)
376
+ return
377
+ }
378
+
379
+ remote, downloadURL, err := u.getSupervisorRelease(ctx)
380
+ if err != nil {
381
+ u.log.Warn("Failed to check supervisor release: %v", err)
382
+ return
383
+ }
384
+
385
+ local := u.getLocalSupervisorVersion()
386
+ if local == "" {
387
+ u.log.Info("No local supervisor version recorded — storing %s", remote)
388
+ u.setLocalSupervisorVersion(remote)
389
+ return
390
+ }
391
+
392
+ if local == remote {
393
+ u.log.Debug("Supervisor updater: version %s is up to date", local)
394
+ return
395
+ }
396
+
397
+ u.log.Info("Supervisor update available: %s → %s", local, remote)
398
+ coordLock, ok := AcquireUpdateCoordinatorLock(u.cfg.Paths.UpdateApplyLock, updateScopeSupervisor, u.log)
399
+ if !ok {
400
+ u.log.Info("Deferring supervisor binary update %s → %s due to active update apply lock", local, remote)
401
+ return
402
+ }
403
+ defer coordLock.Release()
404
+
405
+ markFailed := func(err error) {
406
+ u.state.Transition(updateScopeSupervisor, updatePhaseFailed, remote, local, err.Error())
407
+ }
408
+
409
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating binary %s → %s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
410
+
411
+ select {
412
+ case <-ctx.Done():
413
+ markFailed(ctx.Err())
414
+ return
415
+ case <-time.After(u.cfg.GracePeriod):
416
+ }
417
+
418
+ if err := u.downloadSupervisorBinary(ctx, downloadURL); err != nil {
419
+ markFailed(err)
420
+ u.log.Error("Supervisor binary download failed: %v", err)
421
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: binary update to %s failed during download.", remote), 0)
422
+ return
423
+ }
424
+
425
+ if err := u.stagePendingSupervisorVersion(remote); err != nil {
426
+ _ = os.Remove(u.cfg.Paths.PendingBinary)
427
+ markFailed(err)
428
+ u.log.Error("Failed to stage supervisor version %s: %v", remote, err)
429
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: binary update to %s failed during staging.", remote), 0)
430
+ return
431
+ }
432
+ u.state.Transition(updateScopeSupervisor, updatePhaseStaged, remote, local, "")
433
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: downloaded %s. Restarting supervisor to apply update...", remote), 0)
434
+
435
+ isService, err := isWindowsService()
436
+ if err != nil {
437
+ markFailed(err)
438
+ u.log.Error("Failed to detect service mode for restart: %v", err)
439
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but service detection failed.", 0)
440
+ return
441
+ }
442
+ u.state.Transition(updateScopeSupervisor, updatePhaseRestarting, remote, local, "")
443
+
444
+ if isService {
445
+ if err := scheduleServiceRestartForUpdate(u.log); err != nil {
446
+ markFailed(err)
447
+ u.log.Error("Failed to schedule service restart: %v", err)
448
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but service restart scheduling failed.", 0)
449
+ }
450
+ return
451
+ }
452
+
453
+ if err := signalSelf(syscall.SIGTERM); err != nil {
454
+ markFailed(err)
455
+ u.log.Error("Failed to signal supervisor for restart: %v", err)
456
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but restart signal failed.", 0)
457
+ }
458
+ }
459
+
460
+ func (u *Updater) downloadSupervisorBinary(ctx context.Context, downloadURL string) error {
461
+ if err := os.MkdirAll(u.cfg.Paths.BinaryDir, 0755); err != nil {
462
+ return fmt.Errorf("create binary dir: %w", err)
463
+ }
464
+
465
+ tmpPath := u.cfg.Paths.PendingBinary + ".download"
466
+ defer os.Remove(tmpPath)
467
+
468
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
469
+ if err != nil {
470
+ return err
471
+ }
472
+ req.Header.Set("User-Agent", "sensorium-supervisor-updater")
473
+
474
+ resp, err := http.DefaultClient.Do(req)
475
+ if err != nil {
476
+ return err
477
+ }
478
+ defer resp.Body.Close()
479
+
480
+ if resp.StatusCode != http.StatusOK {
481
+ return fmt.Errorf("download HTTP %d", resp.StatusCode)
482
+ }
483
+
484
+ f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
485
+ if err != nil {
486
+ return err
487
+ }
488
+
489
+ written, copyErr := io.Copy(f, resp.Body)
490
+ closeErr := f.Close()
491
+ if copyErr != nil {
492
+ return copyErr
493
+ }
494
+ if closeErr != nil {
495
+ return closeErr
496
+ }
497
+ if written <= 0 {
498
+ return fmt.Errorf("downloaded empty binary")
499
+ }
500
+
501
+ info, err := os.Stat(tmpPath)
502
+ if err != nil {
503
+ return err
504
+ }
505
+ if info.Size() <= 0 {
506
+ return fmt.Errorf("downloaded binary has invalid size %d", info.Size())
507
+ }
508
+
509
+ if err := os.Remove(u.cfg.Paths.PendingBinary); err != nil && !os.IsNotExist(err) {
510
+ return err
511
+ }
512
+ if err := os.Rename(tmpPath, u.cfg.Paths.PendingBinary); err != nil {
513
+ return err
514
+ }
515
+
516
+ u.log.Info("Supervisor binary downloaded to %s (%d bytes)", u.cfg.Paths.PendingBinary, info.Size())
517
+ return nil
518
+ }
519
+
520
+ func signalSelf(sig os.Signal) error {
521
+ proc, err := os.FindProcess(os.Getpid())
522
+ if err != nil {
523
+ return err
524
+ }
525
+ return proc.Signal(sig)
526
+ }
527
+
227
528
  func (u *Updater) killServer() {
228
529
  u.log.Info("Updater: stopping current MCP server for update")
229
530
  pid, err := ReadPIDFile(u.cfg.Paths.ServerPID)
@@ -0,0 +1,64 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+ )
10
+
11
+ func TestVerifyUpdatedMCPServerReady_FailureSetsFailedStateAndNoSuccessMessage(t *testing.T) {
12
+ dir := t.TempDir()
13
+ log := NewLogger(filepath.Join(dir, "test.log"))
14
+ defer log.Close()
15
+
16
+ cfg := Config{
17
+ DataDir: dir,
18
+ Paths: Paths{
19
+ UpdateState: filepath.Join(dir, "update-state.json"),
20
+ },
21
+ }
22
+
23
+ u := NewUpdater(cfg, NewMCPClient(1, ""), log)
24
+ u.state = NewUpdateStateStore(cfg.Paths.UpdateState, log)
25
+
26
+ origNotify := notifyUpdaterOperator
27
+ origPoll := mcpUpdateReadyPollInterval
28
+ origTimeout := mcpUpdateReadyTimeout
29
+ defer func() {
30
+ notifyUpdaterOperator = origNotify
31
+ mcpUpdateReadyPollInterval = origPoll
32
+ mcpUpdateReadyTimeout = origTimeout
33
+ }()
34
+
35
+ mcpUpdateReadyPollInterval = 1 * time.Millisecond
36
+ mcpUpdateReadyTimeout = 5 * time.Millisecond
37
+
38
+ var messages []string
39
+ notifyUpdaterOperator = func(_ Config, _ *Logger, text string, _ int) {
40
+ messages = append(messages, text)
41
+ }
42
+
43
+ ok := u.verifyUpdatedMCPServerReady(context.Background(), "2.0.0", "1.0.0", 4242)
44
+ if ok {
45
+ t.Fatal("expected verification to fail")
46
+ }
47
+
48
+ state, err := u.state.Load()
49
+ if err != nil {
50
+ t.Fatalf("load update state: %v", err)
51
+ }
52
+ if state.Phase != updatePhaseFailed {
53
+ t.Fatalf("state phase = %q, want %q", state.Phase, updatePhaseFailed)
54
+ }
55
+ if !strings.Contains(state.LastError, "did not become ready") {
56
+ t.Fatalf("last error = %q, want readiness failure detail", state.LastError)
57
+ }
58
+ if len(messages) == 0 {
59
+ t.Fatal("expected failure notification message")
60
+ }
61
+ if strings.Contains(messages[len(messages)-1], "complete") {
62
+ t.Fatalf("unexpected success message: %q", messages[len(messages)-1])
63
+ }
64
+ }
@@ -1,32 +0,0 @@
1
- /**
2
- * Memory quality scoring — samples random active notes and scores them
3
- * via LLM on specificity, actionability, and accuracy.
4
- *
5
- * Results are persisted in a `quality_scores` table for trend tracking.
6
- */
7
- import type { Database } from "./schema.js";
8
- interface NoteScore {
9
- noteId: string;
10
- content: string;
11
- specificity: number;
12
- actionability: number;
13
- accuracy: number;
14
- avg: number;
15
- }
16
- export interface QualityScoreResult {
17
- id: string;
18
- scoredAt: string;
19
- sampleSize: number;
20
- avgSpecificity: number;
21
- avgActionability: number;
22
- avgAccuracy: number;
23
- overallAvg: number;
24
- details: NoteScore[];
25
- }
26
- export interface ScoreOptions {
27
- sampleSize?: number;
28
- }
29
- export declare function scoreMemoryQuality(db: Database, options?: ScoreOptions): Promise<QualityScoreResult>;
30
- export declare function formatScoreComparison(db: Database, current: QualityScoreResult): string;
31
- export {};
32
- //# sourceMappingURL=quality-scoring.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"quality-scoring.d.ts","sourceRoot":"","sources":["../../../src/data/memory/quality-scoring.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAiB5C,UAAU,SAAS;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAwJD,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,QAAQ,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAmD7B;AAID,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,kBAAkB,GAC1B,MAAM,CAqCR"}
@@ -1,182 +0,0 @@
1
- /**
2
- * Memory quality scoring — samples random active notes and scores them
3
- * via LLM on specificity, actionability, and accuracy.
4
- *
5
- * Results are persisted in a `quality_scores` table for trend tracking.
6
- */
7
- import { generateId, nowISO, repairAndParseJSON } from "./utils.js";
8
- import { chatCompletion } from "../../integrations/openai/chat.js";
9
- import { log } from "../../logger.js";
10
- // ─── Constants ───────────────────────────────────────────────────────────────
11
- const DEFAULT_SAMPLE_SIZE = 20;
12
- const SCORING_MODEL_ENV = "QUALITY_SCORING_MODEL";
13
- const FALLBACK_MODEL_ENV = "CONSOLIDATION_MODEL";
14
- const DEFAULT_MODEL = "gpt-4o-mini";
15
- const MAX_TOKENS = 2048;
16
- const TIMEOUT_MS = 60_000;
17
- // ─── Schema ──────────────────────────────────────────────────────────────────
18
- function ensureQualityScoresTable(db) {
19
- db.exec(`
20
- CREATE TABLE IF NOT EXISTS quality_scores (
21
- id TEXT PRIMARY KEY,
22
- scored_at TEXT NOT NULL,
23
- sample_size INTEGER NOT NULL,
24
- avg_specificity REAL,
25
- avg_actionability REAL,
26
- avg_accuracy REAL,
27
- overall_avg REAL,
28
- details TEXT
29
- )
30
- `);
31
- }
32
- // ─── LLM Prompt ──────────────────────────────────────────────────────────────
33
- function buildScoringPrompt(notes) {
34
- const notesList = notes
35
- .map((n, i) => `${i + 1}. [${n.noteId}] ${n.content}`)
36
- .join("\n");
37
- return [
38
- {
39
- role: "system",
40
- content: [
41
- "You are a memory quality auditor. Score each note on three dimensions (1-5 each):",
42
- "- **Specificity**: 1=vague platitude, 5=precise actionable fact",
43
- "- **Actionability**: 1=useless to a future agent, 5=directly actionable",
44
- "- **Accuracy**: 1=garbled/contradictory, 5=clear and coherent",
45
- "",
46
- "Respond ONLY with valid JSON: { \"scores\": [ { \"noteId\": \"...\", \"specificity\": N, \"actionability\": N, \"accuracy\": N } ] }",
47
- ].join("\n"),
48
- },
49
- {
50
- role: "user",
51
- content: `Score these ${notes.length} memory notes:\n\n${notesList}`,
52
- },
53
- ];
54
- }
55
- // ─── Sampling ────────────────────────────────────────────────────────────────
56
- function sampleActiveNotes(db, count) {
57
- const rows = db.prepare(`SELECT note_id AS noteId, content
58
- FROM semantic_notes
59
- WHERE valid_to IS NULL AND superseded_by IS NULL
60
- ORDER BY RANDOM()
61
- LIMIT ?`).all(count);
62
- return rows;
63
- }
64
- // ─── Persistence ─────────────────────────────────────────────────────────────
65
- function saveScoreResult(db, result) {
66
- db.prepare(`INSERT INTO quality_scores (id, scored_at, sample_size, avg_specificity, avg_actionability, avg_accuracy, overall_avg, details)
67
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(result.id, result.scoredAt, result.sampleSize, result.avgSpecificity, result.avgActionability, result.avgAccuracy, result.overallAvg, JSON.stringify(result.details));
68
- }
69
- function getLastRun(db) {
70
- const row = db.prepare(`SELECT id, scored_at, overall_avg
71
- FROM quality_scores
72
- ORDER BY scored_at DESC
73
- LIMIT 1`).get();
74
- return row;
75
- }
76
- // ─── Aggregation ─────────────────────────────────────────────────────────────
77
- function aggregateScores(notes, rawScores) {
78
- const scoreMap = new Map(rawScores.map(s => [s.noteId, s]));
79
- return notes
80
- .filter(n => scoreMap.has(n.noteId))
81
- .map(n => {
82
- const s = scoreMap.get(n.noteId);
83
- return {
84
- noteId: n.noteId,
85
- content: n.content,
86
- specificity: clampScore(s.specificity),
87
- actionability: clampScore(s.actionability),
88
- accuracy: clampScore(s.accuracy),
89
- avg: round((clampScore(s.specificity) + clampScore(s.actionability) + clampScore(s.accuracy)) / 3),
90
- };
91
- });
92
- }
93
- function clampScore(v) {
94
- return Math.max(1, Math.min(5, Math.round(v)));
95
- }
96
- function round(v) {
97
- return Math.round(v * 100) / 100;
98
- }
99
- function computeAverages(details) {
100
- if (details.length === 0) {
101
- return { avgSpecificity: 0, avgActionability: 0, avgAccuracy: 0, overallAvg: 0 };
102
- }
103
- const sum = details.reduce((acc, d) => ({
104
- s: acc.s + d.specificity,
105
- a: acc.a + d.actionability,
106
- c: acc.c + d.accuracy,
107
- }), { s: 0, a: 0, c: 0 });
108
- const n = details.length;
109
- const avgSpecificity = round(sum.s / n);
110
- const avgActionability = round(sum.a / n);
111
- const avgAccuracy = round(sum.c / n);
112
- const overallAvg = round((avgSpecificity + avgActionability + avgAccuracy) / 3);
113
- return { avgSpecificity, avgActionability, avgAccuracy, overallAvg };
114
- }
115
- export async function scoreMemoryQuality(db, options) {
116
- ensureQualityScoresTable(db);
117
- const sampleSize = options?.sampleSize ?? DEFAULT_SAMPLE_SIZE;
118
- const notes = sampleActiveNotes(db, sampleSize);
119
- if (notes.length === 0) {
120
- throw new Error("No active semantic notes to score.");
121
- }
122
- const apiKey = process.env.OPENAI_API_KEY;
123
- if (!apiKey) {
124
- throw new Error("OPENAI_API_KEY not set — cannot score memory quality.");
125
- }
126
- const model = process.env[SCORING_MODEL_ENV] ??
127
- process.env[FALLBACK_MODEL_ENV] ??
128
- DEFAULT_MODEL;
129
- const messages = buildScoringPrompt(notes);
130
- const raw = await chatCompletion(messages, apiKey, {
131
- model,
132
- maxTokens: MAX_TOKENS,
133
- responseFormat: { type: "json_object" },
134
- timeoutMs: TIMEOUT_MS,
135
- });
136
- const parsed = repairAndParseJSON(raw);
137
- if (!parsed.scores || !Array.isArray(parsed.scores)) {
138
- throw new Error("LLM returned invalid scoring response — missing 'scores' array.");
139
- }
140
- const details = aggregateScores(notes, parsed.scores);
141
- const averages = computeAverages(details);
142
- const result = {
143
- id: generateId("qs"),
144
- scoredAt: nowISO(),
145
- sampleSize: notes.length,
146
- ...averages,
147
- details,
148
- };
149
- saveScoreResult(db, result);
150
- log.info(`[quality-scoring] Scored ${details.length} notes — overall avg: ${result.overallAvg}`);
151
- return result;
152
- }
153
- // ─── Comparison Helper ───────────────────────────────────────────────────────
154
- export function formatScoreComparison(db, current) {
155
- ensureQualityScoresTable(db);
156
- const prev = db.prepare(`SELECT id, scored_at, overall_avg
157
- FROM quality_scores
158
- WHERE id != ?
159
- ORDER BY scored_at DESC
160
- LIMIT 1`).get(current.id);
161
- const lines = [
162
- "## Memory Quality Score",
163
- `- **Specificity**: ${current.avgSpecificity}/5`,
164
- `- **Actionability**: ${current.avgActionability}/5`,
165
- `- **Accuracy**: ${current.avgAccuracy}/5`,
166
- `- **Overall**: ${current.overallAvg}/5`,
167
- `- Sample size: ${current.sampleSize} notes`,
168
- ];
169
- if (prev) {
170
- const delta = round(current.overallAvg - prev.overall_avg);
171
- const pct = prev.overall_avg > 0
172
- ? round((delta / prev.overall_avg) * 100)
173
- : 0;
174
- const direction = delta > 0 ? "improved" : delta < 0 ? "declined" : "unchanged";
175
- lines.push("", `### Comparison to previous run`, `- Previous overall: ${prev.overall_avg}/5 (${prev.scored_at})`, `- Change: ${delta > 0 ? "+" : ""}${delta} (${direction} by ${Math.abs(pct)}%)`);
176
- }
177
- else {
178
- lines.push("", "_First run — no previous data for comparison._");
179
- }
180
- return lines.join("\n");
181
- }
182
- //# sourceMappingURL=quality-scoring.js.map