sensorium-mcp 2.17.28 → 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 (208) 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 -25
  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 -554
  34. package/dist/data/memory/consolidation.js.map +1 -1
  35. package/dist/data/memory/migration-runner.d.ts +5 -0
  36. package/dist/data/memory/migration-runner.d.ts.map +1 -0
  37. package/dist/data/memory/migration-runner.js +403 -0
  38. package/dist/data/memory/migration-runner.js.map +1 -0
  39. package/dist/data/memory/reflection.js +1 -1
  40. package/dist/data/memory/schema-ddl.d.ts +4 -0
  41. package/dist/data/memory/schema-ddl.d.ts.map +1 -0
  42. package/dist/data/memory/schema-ddl.js +194 -0
  43. package/dist/data/memory/schema-ddl.js.map +1 -0
  44. package/dist/data/memory/schema-guard.d.ts +3 -0
  45. package/dist/data/memory/schema-guard.d.ts.map +1 -0
  46. package/dist/data/memory/schema-guard.js +184 -0
  47. package/dist/data/memory/schema-guard.js.map +1 -0
  48. package/dist/data/memory/schema.d.ts +2 -5
  49. package/dist/data/memory/schema.d.ts.map +1 -1
  50. package/dist/data/memory/schema.js +6 -834
  51. package/dist/data/memory/schema.js.map +1 -1
  52. package/dist/data/memory/synthesis.js +2 -2
  53. package/dist/data/memory/synthesis.js.map +1 -1
  54. package/dist/data/memory/thread-registry.d.ts +18 -4
  55. package/dist/data/memory/thread-registry.d.ts.map +1 -1
  56. package/dist/data/memory/thread-registry.js +25 -0
  57. package/dist/data/memory/thread-registry.js.map +1 -1
  58. package/dist/data/sent-message.repository.d.ts +12 -0
  59. package/dist/data/sent-message.repository.d.ts.map +1 -0
  60. package/dist/data/sent-message.repository.js +31 -0
  61. package/dist/data/sent-message.repository.js.map +1 -0
  62. package/dist/http-server.d.ts.map +1 -1
  63. package/dist/http-server.js +23 -2
  64. package/dist/http-server.js.map +1 -1
  65. package/dist/index.js +27 -48
  66. package/dist/index.js.map +1 -1
  67. package/dist/logger.d.ts +7 -2
  68. package/dist/logger.d.ts.map +1 -1
  69. package/dist/logger.js +89 -12
  70. package/dist/logger.js.map +1 -1
  71. package/dist/scheduler.d.ts +8 -0
  72. package/dist/scheduler.d.ts.map +1 -1
  73. package/dist/scheduler.js +15 -0
  74. package/dist/scheduler.js.map +1 -1
  75. package/dist/server/factory.d.ts +2 -1
  76. package/dist/server/factory.d.ts.map +1 -1
  77. package/dist/server/factory.js +11 -4
  78. package/dist/server/factory.js.map +1 -1
  79. package/dist/services/agent-spawn.service.d.ts +39 -0
  80. package/dist/services/agent-spawn.service.d.ts.map +1 -0
  81. package/dist/services/agent-spawn.service.js +348 -0
  82. package/dist/services/agent-spawn.service.js.map +1 -0
  83. package/dist/services/background-runner.d.ts +26 -0
  84. package/dist/services/background-runner.d.ts.map +1 -0
  85. package/dist/services/background-runner.js +71 -0
  86. package/dist/services/background-runner.js.map +1 -0
  87. package/dist/services/consolidation.service.d.ts +16 -0
  88. package/dist/services/consolidation.service.d.ts.map +1 -0
  89. package/dist/services/consolidation.service.js +508 -0
  90. package/dist/services/consolidation.service.js.map +1 -0
  91. package/dist/services/dispatcher/broker.d.ts +2 -0
  92. package/dist/services/dispatcher/broker.d.ts.map +1 -1
  93. package/dist/services/dispatcher/broker.js +5 -10
  94. package/dist/services/dispatcher/broker.js.map +1 -1
  95. package/dist/services/dispatcher/index.d.ts +1 -1
  96. package/dist/services/dispatcher/index.d.ts.map +1 -1
  97. package/dist/services/dispatcher/index.js +1 -1
  98. package/dist/services/dispatcher/index.js.map +1 -1
  99. package/dist/services/dispatcher/lock.d.ts.map +1 -1
  100. package/dist/services/dispatcher/lock.js +7 -11
  101. package/dist/services/dispatcher/lock.js.map +1 -1
  102. package/dist/services/maintenance-signal.d.ts +18 -0
  103. package/dist/services/maintenance-signal.d.ts.map +1 -0
  104. package/dist/services/maintenance-signal.js +48 -0
  105. package/dist/services/maintenance-signal.js.map +1 -0
  106. package/dist/services/memory-briefing.service.d.ts +4 -0
  107. package/dist/services/memory-briefing.service.d.ts.map +1 -0
  108. package/dist/services/memory-briefing.service.js +143 -0
  109. package/dist/services/memory-briefing.service.js.map +1 -0
  110. package/dist/services/process.service.d.ts +31 -0
  111. package/dist/services/process.service.d.ts.map +1 -0
  112. package/dist/services/process.service.js +100 -0
  113. package/dist/services/process.service.js.map +1 -0
  114. package/dist/services/thread-health.service.d.ts +18 -0
  115. package/dist/services/thread-health.service.d.ts.map +1 -0
  116. package/dist/services/thread-health.service.js +118 -0
  117. package/dist/services/thread-health.service.js.map +1 -0
  118. package/dist/services/thread-lifecycle.service.d.ts +52 -0
  119. package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
  120. package/dist/services/thread-lifecycle.service.js +174 -0
  121. package/dist/services/thread-lifecycle.service.js.map +1 -0
  122. package/dist/services/topic.service.d.ts +25 -0
  123. package/dist/services/topic.service.d.ts.map +1 -0
  124. package/dist/services/topic.service.js +65 -0
  125. package/dist/services/topic.service.js.map +1 -0
  126. package/dist/services/worker-cleanup.service.d.ts +8 -0
  127. package/dist/services/worker-cleanup.service.d.ts.map +1 -0
  128. package/dist/services/worker-cleanup.service.js +82 -0
  129. package/dist/services/worker-cleanup.service.js.map +1 -0
  130. package/dist/sessions.d.ts +14 -0
  131. package/dist/sessions.d.ts.map +1 -1
  132. package/dist/sessions.js +55 -0
  133. package/dist/sessions.js.map +1 -1
  134. package/dist/telegram.d.ts +13 -6
  135. package/dist/telegram.d.ts.map +1 -1
  136. package/dist/telegram.js +43 -14
  137. package/dist/telegram.js.map +1 -1
  138. package/dist/tools/delegate-tool.d.ts +4 -0
  139. package/dist/tools/delegate-tool.d.ts.map +1 -1
  140. package/dist/tools/delegate-tool.js +48 -109
  141. package/dist/tools/delegate-tool.js.map +1 -1
  142. package/dist/tools/memory-tools.d.ts.map +1 -1
  143. package/dist/tools/memory-tools.js +1 -1
  144. package/dist/tools/memory-tools.js.map +1 -1
  145. package/dist/tools/shared-agent-utils.d.ts +9 -1
  146. package/dist/tools/shared-agent-utils.d.ts.map +1 -1
  147. package/dist/tools/shared-agent-utils.js +21 -38
  148. package/dist/tools/shared-agent-utils.js.map +1 -1
  149. package/dist/tools/start-session-tool.d.ts +2 -0
  150. package/dist/tools/start-session-tool.d.ts.map +1 -1
  151. package/dist/tools/start-session-tool.js +66 -106
  152. package/dist/tools/start-session-tool.js.map +1 -1
  153. package/dist/tools/thread-lifecycle.d.ts +5 -127
  154. package/dist/tools/thread-lifecycle.d.ts.map +1 -1
  155. package/dist/tools/thread-lifecycle.js +5 -1167
  156. package/dist/tools/thread-lifecycle.js.map +1 -1
  157. package/dist/tools/utility-tools.js +5 -2
  158. package/dist/tools/utility-tools.js.map +1 -1
  159. package/dist/tools/wait/drive-handler.d.ts +0 -1
  160. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  161. package/dist/tools/wait/drive-handler.js +5 -22
  162. package/dist/tools/wait/drive-handler.js.map +1 -1
  163. package/dist/tools/wait/message-delivery.js +1 -1
  164. package/dist/tools/wait/message-delivery.js.map +1 -1
  165. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  166. package/dist/tools/wait/message-processing.js +9 -8
  167. package/dist/tools/wait/message-processing.js.map +1 -1
  168. package/dist/tools/wait/poll-loop.d.ts +2 -0
  169. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  170. package/dist/tools/wait/poll-loop.js +27 -29
  171. package/dist/tools/wait/poll-loop.js.map +1 -1
  172. package/dist/tools/wait/task-handler.d.ts +0 -3
  173. package/dist/tools/wait/task-handler.d.ts.map +1 -1
  174. package/dist/tools/wait/task-handler.js +3 -2
  175. package/dist/tools/wait/task-handler.js.map +1 -1
  176. package/dist/types.d.ts +0 -1
  177. package/dist/types.d.ts.map +1 -1
  178. package/package.json +4 -8
  179. package/supervisor/config.go +182 -69
  180. package/supervisor/config_test.go +78 -0
  181. package/supervisor/go.mod +12 -0
  182. package/supervisor/go.sum +20 -0
  183. package/supervisor/health.go +56 -6
  184. package/supervisor/health_test.go +29 -0
  185. package/supervisor/keeper.go +15 -10
  186. package/supervisor/log.go +109 -28
  187. package/supervisor/log_test.go +86 -6
  188. package/supervisor/main.go +150 -19
  189. package/supervisor/main_test.go +130 -0
  190. package/supervisor/process.go +47 -4
  191. package/supervisor/process_test.go +14 -0
  192. package/supervisor/secrets.go +95 -0
  193. package/supervisor/secrets_securevault_test.go +98 -0
  194. package/supervisor/secrets_test.go +119 -0
  195. package/supervisor/self_update.go +282 -0
  196. package/supervisor/self_update_test.go +177 -0
  197. package/supervisor/service_restart_stub.go +9 -0
  198. package/supervisor/service_restart_windows.go +63 -0
  199. package/supervisor/service_stub.go +15 -0
  200. package/supervisor/service_windows.go +216 -0
  201. package/supervisor/update_state.go +264 -0
  202. package/supervisor/update_state_test.go +306 -0
  203. package/supervisor/updater.go +311 -10
  204. package/supervisor/updater_test.go +64 -0
  205. package/scripts/install-supervisor.ps1 +0 -67
  206. package/scripts/install-supervisor.sh +0 -43
  207. package/scripts/start-supervisor.ps1 +0 -46
  208. package/scripts/start-supervisor.sh +0 -20
@@ -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,67 +0,0 @@
1
- #!/usr/bin/env pwsh
2
- <#
3
- .SYNOPSIS
4
- Build and install the sensorium-supervisor Go binary.
5
- .DESCRIPTION
6
- Compiles the Go supervisor and places it in ~/.remote-copilot-mcp/bin/.
7
- Requires Go 1.22+ installed and on PATH.
8
- .PARAMETER Force
9
- Rebuild even if the binary already exists.
10
- #>
11
- param(
12
- [switch]$Force
13
- )
14
-
15
- $ErrorActionPreference = "Stop"
16
-
17
- $DataDir = Join-Path $env:USERPROFILE ".remote-copilot-mcp"
18
- $BinDir = Join-Path $DataDir "bin"
19
- $Binary = Join-Path $BinDir "sensorium-supervisor.exe"
20
-
21
- # Find the supervisor source directory (relative to this script)
22
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
23
- $SupervisorDir = Join-Path (Split-Path -Parent $ScriptDir) "supervisor"
24
-
25
- if (-not (Test-Path (Join-Path $SupervisorDir "go.mod"))) {
26
- Write-Error "Cannot find supervisor source at $SupervisorDir"
27
- exit 1
28
- }
29
-
30
- # Check if Go is available
31
- $goExe = Get-Command go -ErrorAction SilentlyContinue
32
- if (-not $goExe) {
33
- Write-Host "Go is not installed. Install from https://go.dev/dl/ (requires Go 1.22+)" -ForegroundColor Red
34
- exit 1
35
- }
36
-
37
- # Check version
38
- $goVersion = (go version) -replace 'go version go', '' -replace ' .*', ''
39
- Write-Host "Found Go $goVersion"
40
-
41
- # Skip build if binary exists and is newer than source (unless -Force)
42
- if (-not $Force -and (Test-Path $Binary)) {
43
- $binaryTime = (Get-Item $Binary).LastWriteTime
44
- $sourceFiles = Get-ChildItem $SupervisorDir -Filter "*.go"
45
- $newestSource = ($sourceFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1).LastWriteTime
46
- if ($binaryTime -gt $newestSource) {
47
- Write-Host "sensorium-supervisor is up to date ($Binary)"
48
- exit 0
49
- }
50
- }
51
-
52
- # Ensure bin directory exists
53
- New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
54
-
55
- Write-Host "Building sensorium-supervisor..."
56
- Push-Location $SupervisorDir
57
- try {
58
- go build -o $Binary .
59
- if ($LASTEXITCODE -ne 0) {
60
- Write-Error "Go build failed"
61
- exit 1
62
- }
63
- } finally {
64
- Pop-Location
65
- }
66
-
67
- Write-Host "Installed: $Binary" -ForegroundColor Green
@@ -1,43 +0,0 @@
1
- #!/bin/sh
2
- # Build and install the sensorium-supervisor Go binary.
3
- # Requires Go 1.22+ installed and on PATH.
4
- set -e
5
-
6
- FORCE="${1:-}"
7
- DATA_DIR="$HOME/.remote-copilot-mcp"
8
- BIN_DIR="$DATA_DIR/bin"
9
- BINARY="$BIN_DIR/sensorium-supervisor"
10
-
11
- # Find supervisor source relative to this script
12
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
- SUPERVISOR_DIR="$(dirname "$SCRIPT_DIR")/supervisor"
14
-
15
- if [ ! -f "$SUPERVISOR_DIR/go.mod" ]; then
16
- echo "ERROR: Cannot find supervisor source at $SUPERVISOR_DIR" >&2
17
- exit 1
18
- fi
19
-
20
- # Check Go is available
21
- if ! command -v go >/dev/null 2>&1; then
22
- echo "ERROR: Go is not installed. Install from https://go.dev/dl/ (requires Go 1.22+)" >&2
23
- exit 1
24
- fi
25
-
26
- echo "Found $(go version)"
27
-
28
- # Skip if binary is newer than source (unless --force)
29
- if [ "$FORCE" != "--force" ] && [ -f "$BINARY" ]; then
30
- NEWEST_SRC=$(find "$SUPERVISOR_DIR" -name '*.go' -newer "$BINARY" 2>/dev/null | head -1)
31
- if [ -z "$NEWEST_SRC" ]; then
32
- echo "sensorium-supervisor is up to date ($BINARY)"
33
- exit 0
34
- fi
35
- fi
36
-
37
- mkdir -p "$BIN_DIR"
38
-
39
- echo "Building sensorium-supervisor..."
40
- cd "$SUPERVISOR_DIR"
41
- go build -o "$BINARY" .
42
-
43
- echo "Installed: $BINARY"
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env pwsh
2
- <#
3
- .SYNOPSIS
4
- Launch sensorium-supervisor. Builds automatically if needed.
5
- .DESCRIPTION
6
- Replaces update-watcher.ps1. Builds the Go supervisor if it doesn't exist,
7
- then runs it. All environment variables (MCP_HTTP_PORT, TELEGRAM_TOKEN, etc.)
8
- are passed through to the supervisor process.
9
- .PARAMETER Mode
10
- Watcher mode: production or development. Maps to WATCHER_MODE env var.
11
- .PARAMETER Build
12
- Force rebuild of the supervisor binary before starting.
13
- #>
14
- param(
15
- [ValidateSet("production", "development")]
16
- [string]$Mode = "production",
17
- [switch]$Build
18
- )
19
-
20
- $ErrorActionPreference = "Stop"
21
-
22
- $DataDir = Join-Path $env:USERPROFILE ".remote-copilot-mcp"
23
- $BinDir = Join-Path $DataDir "bin"
24
- $Binary = Join-Path $BinDir "sensorium-supervisor.exe"
25
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
26
-
27
- # Build if missing or requested
28
- if ($Build -or -not (Test-Path $Binary)) {
29
- $installScript = Join-Path $ScriptDir "install-supervisor.ps1"
30
- if ($Build) {
31
- & $installScript -Force
32
- } else {
33
- & $installScript
34
- }
35
- if ($LASTEXITCODE -ne 0) { exit 1 }
36
- }
37
-
38
- # Set WATCHER_MODE if not already set
39
- if (-not $env:WATCHER_MODE) {
40
- $env:WATCHER_MODE = $Mode
41
- }
42
-
43
- # Launch supervisor
44
- Write-Host "Starting sensorium-supervisor ($Mode mode)..."
45
- & $Binary
46
- exit $LASTEXITCODE
@@ -1,20 +0,0 @@
1
- #!/bin/sh
2
- # Launch sensorium-supervisor. Builds automatically if needed.
3
- # Replaces update-watcher.ps1 on Unix systems.
4
- set -e
5
-
6
- MODE="${1:-production}"
7
- DATA_DIR="$HOME/.remote-copilot-mcp"
8
- BIN_DIR="$DATA_DIR/bin"
9
- BINARY="$BIN_DIR/sensorium-supervisor"
10
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
-
12
- # Build if missing
13
- if [ ! -f "$BINARY" ]; then
14
- "$SCRIPT_DIR/install-supervisor.sh"
15
- fi
16
-
17
- export WATCHER_MODE="${WATCHER_MODE:-$MODE}"
18
-
19
- echo "Starting sensorium-supervisor ($WATCHER_MODE mode)..."
20
- exec "$BINARY"