sensorium-mcp 2.17.28 → 3.0.1

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 (207) hide show
  1. package/Install-Sensorium.ps1 +351 -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 +68 -118
  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 +24 -6
  184. package/supervisor/keeper.go +15 -10
  185. package/supervisor/log.go +109 -28
  186. package/supervisor/log_test.go +86 -6
  187. package/supervisor/main.go +146 -19
  188. package/supervisor/main_test.go +130 -0
  189. package/supervisor/process.go +47 -4
  190. package/supervisor/process_test.go +14 -0
  191. package/supervisor/secrets.go +95 -0
  192. package/supervisor/secrets_securevault_test.go +98 -0
  193. package/supervisor/secrets_test.go +119 -0
  194. package/supervisor/self_update.go +282 -0
  195. package/supervisor/self_update_test.go +177 -0
  196. package/supervisor/service_restart_stub.go +9 -0
  197. package/supervisor/service_restart_windows.go +63 -0
  198. package/supervisor/service_stub.go +15 -0
  199. package/supervisor/service_windows.go +216 -0
  200. package/supervisor/update_state.go +264 -0
  201. package/supervisor/update_state_test.go +306 -0
  202. package/supervisor/updater.go +341 -10
  203. package/supervisor/updater_test.go +64 -0
  204. package/scripts/install-supervisor.ps1 +0 -67
  205. package/scripts/install-supervisor.sh +0 -43
  206. package/scripts/start-supervisor.ps1 +0 -46
  207. package/scripts/start-supervisor.sh +0 -20
@@ -4,21 +4,32 @@ import (
4
4
  "context"
5
5
  "encoding/json"
6
6
  "fmt"
7
+ "io"
7
8
  "net/http"
8
9
  "os"
10
+ "os/exec"
9
11
  "path/filepath"
10
12
  "runtime"
11
13
  "strings"
14
+ "syscall"
12
15
  "time"
13
16
  )
14
17
 
15
18
  const registryURL = "https://registry.npmjs.org/sensorium-mcp/latest"
19
+ const supervisorReleaseURL = "https://api.github.com/repos/andriyshevchenko/remote-copilot-mcp/releases/tags/supervisor-latest"
20
+
21
+ var (
22
+ notifyUpdaterOperator = NotifyOperator
23
+ mcpUpdateReadyPollInterval = 3 * time.Second
24
+ mcpUpdateReadyTimeout = 60 * time.Second
25
+ )
16
26
 
17
27
  // Updater checks the npm registry for new versions and performs updates.
18
28
  type Updater struct {
19
29
  cfg Config
20
30
  mcp *MCPClient
21
31
  log *Logger
32
+ state *UpdateStateStore
22
33
  startAt time.Time
23
34
  cancel context.CancelFunc
24
35
  done chan struct{}
@@ -29,6 +40,7 @@ func NewUpdater(cfg Config, mcp *MCPClient, log *Logger) *Updater {
29
40
  cfg: cfg,
30
41
  mcp: mcp,
31
42
  log: log,
43
+ state: NewUpdateStateStore(cfg.Paths.UpdateState, log),
32
44
  startAt: time.Now(),
33
45
  done: make(chan struct{}),
34
46
  }
@@ -75,6 +87,10 @@ func (u *Updater) run(ctx context.Context) {
75
87
  }
76
88
 
77
89
  u.checkAndUpdate(ctx)
90
+ if ctx.Err() != nil {
91
+ return
92
+ }
93
+ u.checkSupervisorUpdate(ctx)
78
94
  }
79
95
  }
80
96
 
@@ -159,30 +175,54 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
159
175
  }
160
176
 
161
177
  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)
178
+ coordLock, ok := AcquireUpdateCoordinatorLock(u.cfg.Paths.UpdateApplyLock, updateScopeMCP, u.log)
179
+ if !ok {
180
+ u.log.Info("Deferring MCP update %s → %s due to active update apply lock", local, remote)
181
+ return
182
+ }
183
+ defer coordLock.Release()
184
+
185
+ u.state.Transition(updateScopeMCP, updatePhaseApplying, remote, local, "")
186
+ markFailed := func(err error) {
187
+ u.state.Transition(updateScopeMCP, updatePhaseFailed, remote, local, err.Error())
188
+ }
189
+
190
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating sensorium v%s → v%s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
163
191
 
164
192
  // Grace period
165
193
  u.log.Info("Grace period %v...", u.cfg.GracePeriod)
166
194
  select {
167
195
  case <-ctx.Done():
196
+ markFailed(ctx.Err())
168
197
  return
169
198
  case <-time.After(u.cfg.GracePeriod):
170
199
  }
171
200
 
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 {
201
+ // Set maintenance flag — always clean up on exit.
202
+ // Written as JSON so TypeScript's checkMaintenanceFlag() can parse the
203
+ // version and timestamp fields for accurate maintenance notifications.
204
+ maintenanceJSON, err := json.Marshal(map[string]string{
205
+ "version": remote,
206
+ "timestamp": time.Now().Format(time.RFC3339),
207
+ })
208
+ if err != nil {
209
+ u.log.Warn("Failed to marshal maintenance flag: %v", err)
210
+ } else if err := atomicWrite(u.cfg.Paths.MaintenanceFlag, maintenanceJSON); err != nil {
174
211
  u.log.Warn("Failed to write maintenance flag: %v", err)
175
212
  }
176
213
  defer os.Remove(u.cfg.Paths.MaintenanceFlag)
177
214
 
178
215
  // Kill the current MCP server
179
216
  if ctx.Err() != nil {
217
+ markFailed(ctx.Err())
180
218
  return
181
219
  }
220
+ u.state.Transition(updateScopeMCP, updatePhaseRestarting, remote, local, "")
182
221
  u.killServer()
183
222
 
184
223
  // Clean npx cache
185
224
  if ctx.Err() != nil {
225
+ markFailed(ctx.Err())
186
226
  return
187
227
  }
188
228
  u.clearNpxCache()
@@ -191,6 +231,7 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
191
231
  var pid int
192
232
  for attempt := 1; attempt <= 3; attempt++ {
193
233
  if ctx.Err() != nil {
234
+ markFailed(ctx.Err())
194
235
  return
195
236
  }
196
237
  pid, err = SpawnMCPServer(u.cfg, u.log)
@@ -204,26 +245,316 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
204
245
  }
205
246
  if err != nil {
206
247
  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)
248
+ markFailed(err)
249
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update FAILED — server is down! Manual intervention required.", 0)
208
250
  return
209
251
  }
210
252
 
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")
253
+ if !u.verifyUpdatedMCPServerReady(ctx, remote, local, pid) {
254
+ return
216
255
  }
217
256
 
218
257
  u.setLocalVersion(remote)
258
+ u.state.Transition(updateScopeMCP, updatePhaseIdle, remote, local, "")
219
259
 
220
- NotifyOperator(u.cfg, u.log, fmt.Sprintf("✅ Supervisor: update to v%s complete. Server ready.", remote), 0)
260
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("✅ Supervisor: update to v%s complete. Server ready.", remote), 0)
221
261
  u.log.Info("Update complete: v%s → v%s", local, remote)
222
262
 
223
263
  // Reset start time for min uptime tracking
224
264
  u.startAt = time.Now()
225
265
  }
226
266
 
267
+ func (u *Updater) verifyUpdatedMCPServerReady(ctx context.Context, remote, local string, pid int) bool {
268
+ u.state.Transition(updateScopeMCP, updatePhaseVerifying, remote, local, "")
269
+ if u.mcp.WaitForReady(ctx, mcpUpdateReadyPollInterval, mcpUpdateReadyTimeout) {
270
+ u.log.Info("Updated MCP server ready (PID %d)", pid)
271
+ return true
272
+ }
273
+
274
+ errMsg := fmt.Sprintf("updated MCP server did not become ready within %v after restart (pid=%d)", mcpUpdateReadyTimeout, pid)
275
+ u.log.Error(errMsg)
276
+ u.state.Transition(updateScopeMCP, updatePhaseFailed, remote, local, errMsg)
277
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: update to v%s FAILED verification. Server did not become ready after restart.", remote), 0)
278
+ return false
279
+ }
280
+
281
+ type githubRelease struct {
282
+ TagName string `json:"tag_name"`
283
+ Name string `json:"name"`
284
+ Assets []struct {
285
+ Name string `json:"name"`
286
+ URL string `json:"browser_download_url"`
287
+ Size int64 `json:"size"`
288
+ } `json:"assets"`
289
+ }
290
+
291
+ func (u *Updater) getSupervisorRelease(ctx context.Context) (string, string, error) {
292
+ ctx2, cancel := context.WithTimeout(ctx, 20*time.Second)
293
+ defer cancel()
294
+
295
+ req, err := http.NewRequestWithContext(ctx2, http.MethodGet, supervisorReleaseURL, nil)
296
+ if err != nil {
297
+ return "", "", err
298
+ }
299
+ req.Header.Set("Accept", "application/vnd.github+json")
300
+ req.Header.Set("User-Agent", "sensorium-supervisor-updater")
301
+
302
+ resp, err := http.DefaultClient.Do(req)
303
+ if err != nil {
304
+ return "", "", err
305
+ }
306
+ defer resp.Body.Close()
307
+
308
+ if resp.StatusCode != http.StatusOK {
309
+ return "", "", fmt.Errorf("GitHub releases HTTP %d", resp.StatusCode)
310
+ }
311
+
312
+ var release githubRelease
313
+ if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
314
+ return "", "", err
315
+ }
316
+
317
+ assetName := supervisorAssetName()
318
+ for _, asset := range release.Assets {
319
+ if asset.Name != assetName {
320
+ continue
321
+ }
322
+
323
+ version := strings.TrimSpace(release.Name)
324
+ if version == "" {
325
+ version = strings.TrimSpace(release.TagName)
326
+ }
327
+ if version == "" {
328
+ return "", "", fmt.Errorf("release version missing for %s", assetName)
329
+ }
330
+ if strings.TrimSpace(asset.URL) == "" {
331
+ return "", "", fmt.Errorf("release asset URL missing for %s", assetName)
332
+ }
333
+
334
+ return version, asset.URL, nil
335
+ }
336
+
337
+ return "", "", fmt.Errorf("release asset %q not found", assetName)
338
+ }
339
+
340
+ func supervisorAssetName() string {
341
+ suffix := ""
342
+ if runtime.GOOS == "windows" {
343
+ suffix = ".exe"
344
+ }
345
+ return fmt.Sprintf("sensorium-supervisor-%s-%s%s", runtime.GOOS, runtime.GOARCH, suffix)
346
+ }
347
+
348
+ func (u *Updater) getLocalSupervisorVersion() string {
349
+ data, err := os.ReadFile(u.cfg.Paths.SupervisorVersion)
350
+ if err != nil {
351
+ return ""
352
+ }
353
+ return strings.TrimSpace(string(data))
354
+ }
355
+
356
+ func (u *Updater) setLocalSupervisorVersion(v string) {
357
+ os.MkdirAll(u.cfg.DataDir, 0755)
358
+ if err := atomicWrite(u.cfg.Paths.SupervisorVersion, []byte(v)); err != nil {
359
+ u.log.Warn("Failed to write supervisor version file: %v", err)
360
+ }
361
+ }
362
+
363
+ func (u *Updater) stagePendingSupervisorVersion(v string) error {
364
+ if err := os.MkdirAll(filepath.Dir(u.cfg.Paths.PendingVersion), 0755); err != nil {
365
+ return fmt.Errorf("create pending supervisor version dir: %w", err)
366
+ }
367
+ if err := atomicWrite(u.cfg.Paths.PendingVersion, []byte(v)); err != nil {
368
+ return fmt.Errorf("write pending supervisor version: %w", err)
369
+ }
370
+ return nil
371
+ }
372
+
373
+ func (u *Updater) checkSupervisorUpdate(ctx context.Context) {
374
+ uptime := time.Since(u.startAt)
375
+ if uptime < u.cfg.MinUptime {
376
+ u.log.Info("Deferring supervisor update — too early (uptime %v < %v)", uptime.Round(time.Second), u.cfg.MinUptime)
377
+ return
378
+ }
379
+
380
+ remote, downloadURL, err := u.getSupervisorRelease(ctx)
381
+ if err != nil {
382
+ u.log.Warn("Failed to check supervisor release: %v", err)
383
+ return
384
+ }
385
+
386
+ local := u.getLocalSupervisorVersion()
387
+ if local == "" {
388
+ u.log.Info("No local supervisor version recorded — storing %s", remote)
389
+ u.setLocalSupervisorVersion(remote)
390
+ return
391
+ }
392
+
393
+ if local == remote {
394
+ u.log.Debug("Supervisor updater: version %s is up to date", local)
395
+ return
396
+ }
397
+
398
+ u.log.Info("Supervisor update available: %s → %s", local, remote)
399
+ coordLock, ok := AcquireUpdateCoordinatorLock(u.cfg.Paths.UpdateApplyLock, updateScopeSupervisor, u.log)
400
+ if !ok {
401
+ u.log.Info("Deferring supervisor binary update %s → %s due to active update apply lock", local, remote)
402
+ return
403
+ }
404
+ defer coordLock.Release()
405
+
406
+ markFailed := func(err error) {
407
+ u.state.Transition(updateScopeSupervisor, updatePhaseFailed, remote, local, err.Error())
408
+ }
409
+
410
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating binary %s → %s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
411
+
412
+ select {
413
+ case <-ctx.Done():
414
+ markFailed(ctx.Err())
415
+ return
416
+ case <-time.After(u.cfg.GracePeriod):
417
+ }
418
+
419
+ if err := u.downloadSupervisorBinary(ctx, downloadURL); err != nil {
420
+ markFailed(err)
421
+ u.log.Error("Supervisor binary download failed: %v", err)
422
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: binary update to %s failed during download.", remote), 0)
423
+ return
424
+ }
425
+
426
+ if err := u.stagePendingSupervisorVersion(remote); err != nil {
427
+ _ = os.Remove(u.cfg.Paths.PendingBinary)
428
+ markFailed(err)
429
+ u.log.Error("Failed to stage supervisor version %s: %v", remote, err)
430
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: binary update to %s failed during staging.", remote), 0)
431
+ return
432
+ }
433
+ u.state.Transition(updateScopeSupervisor, updatePhaseStaged, remote, local, "")
434
+ notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: downloaded %s. Restarting supervisor to apply update...", remote), 0)
435
+
436
+ isService, err := isWindowsService()
437
+ if err != nil {
438
+ markFailed(err)
439
+ u.log.Error("Failed to detect service mode for restart: %v", err)
440
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but service detection failed.", 0)
441
+ return
442
+ }
443
+ u.state.Transition(updateScopeSupervisor, updatePhaseRestarting, remote, local, "")
444
+
445
+ if isService {
446
+ if err := scheduleServiceRestartForUpdate(u.log); err != nil {
447
+ markFailed(err)
448
+ u.log.Error("Failed to schedule service restart: %v", err)
449
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but service restart scheduling failed.", 0)
450
+ }
451
+ return
452
+ }
453
+
454
+ if err := requestSupervisorRestart(u.log); err != nil {
455
+ markFailed(err)
456
+ u.log.Error("Failed to signal supervisor for restart: %v", err)
457
+ notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but restart signal failed.", 0)
458
+ }
459
+ }
460
+
461
+ func (u *Updater) downloadSupervisorBinary(ctx context.Context, downloadURL string) error {
462
+ if err := os.MkdirAll(u.cfg.Paths.BinaryDir, 0755); err != nil {
463
+ return fmt.Errorf("create binary dir: %w", err)
464
+ }
465
+
466
+ tmpPath := u.cfg.Paths.PendingBinary + ".download"
467
+ defer os.Remove(tmpPath)
468
+
469
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
470
+ if err != nil {
471
+ return err
472
+ }
473
+ req.Header.Set("User-Agent", "sensorium-supervisor-updater")
474
+
475
+ resp, err := http.DefaultClient.Do(req)
476
+ if err != nil {
477
+ return err
478
+ }
479
+ defer resp.Body.Close()
480
+
481
+ if resp.StatusCode != http.StatusOK {
482
+ return fmt.Errorf("download HTTP %d", resp.StatusCode)
483
+ }
484
+
485
+ f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
486
+ if err != nil {
487
+ return err
488
+ }
489
+
490
+ written, copyErr := io.Copy(f, resp.Body)
491
+ closeErr := f.Close()
492
+ if copyErr != nil {
493
+ return copyErr
494
+ }
495
+ if closeErr != nil {
496
+ return closeErr
497
+ }
498
+ if written <= 0 {
499
+ return fmt.Errorf("downloaded empty binary")
500
+ }
501
+
502
+ info, err := os.Stat(tmpPath)
503
+ if err != nil {
504
+ return err
505
+ }
506
+ if info.Size() <= 0 {
507
+ return fmt.Errorf("downloaded binary has invalid size %d", info.Size())
508
+ }
509
+
510
+ if err := os.Remove(u.cfg.Paths.PendingBinary); err != nil && !os.IsNotExist(err) {
511
+ return err
512
+ }
513
+ if err := os.Rename(tmpPath, u.cfg.Paths.PendingBinary); err != nil {
514
+ return err
515
+ }
516
+
517
+ u.log.Info("Supervisor binary downloaded to %s (%d bytes)", u.cfg.Paths.PendingBinary, info.Size())
518
+ return nil
519
+ }
520
+
521
+ func signalSelf(sig os.Signal) error {
522
+ proc, err := os.FindProcess(os.Getpid())
523
+ if err != nil {
524
+ return err
525
+ }
526
+ return proc.Signal(sig)
527
+ }
528
+
529
+ func requestSupervisorRestart(log *Logger) error {
530
+ if runtime.GOOS != "windows" {
531
+ return signalSelf(syscall.SIGTERM)
532
+ }
533
+
534
+ exePath, err := os.Executable()
535
+ if err != nil {
536
+ return fmt.Errorf("resolve executable path: %w", err)
537
+ }
538
+
539
+ cmd := exec.Command(exePath)
540
+ cmd.Env = os.Environ()
541
+ setSysProcAttr(cmd)
542
+ if err := cmd.Start(); err != nil {
543
+ return fmt.Errorf("start replacement supervisor: %w", err)
544
+ }
545
+
546
+ if log != nil {
547
+ log.Info("Spawned replacement supervisor process PID %d", cmd.Process.Pid)
548
+ }
549
+
550
+ go func() {
551
+ time.Sleep(250 * time.Millisecond)
552
+ os.Exit(0)
553
+ }()
554
+
555
+ return nil
556
+ }
557
+
227
558
  func (u *Updater) killServer() {
228
559
  u.log.Info("Updater: stopping current MCP server for update")
229
560
  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"