sensorium-mcp 3.0.0 → 3.0.2

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 (42) hide show
  1. package/Install-Sensorium.ps1 +30 -6
  2. package/dist/data/memory/thread-registry.js +1 -1
  3. package/dist/data/memory/thread-registry.js.map +1 -1
  4. package/dist/http-server.d.ts.map +1 -1
  5. package/dist/http-server.js +9 -1
  6. package/dist/http-server.js.map +1 -1
  7. package/dist/index.js +4 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/services/agent-spawn.service.d.ts.map +1 -1
  10. package/dist/services/agent-spawn.service.js +12 -5
  11. package/dist/services/agent-spawn.service.js.map +1 -1
  12. package/dist/services/reconnect-snapshot.service.d.ts +30 -0
  13. package/dist/services/reconnect-snapshot.service.d.ts.map +1 -0
  14. package/dist/services/reconnect-snapshot.service.js +83 -0
  15. package/dist/services/reconnect-snapshot.service.js.map +1 -0
  16. package/dist/services/thread-lifecycle.service.js +1 -1
  17. package/dist/services/thread-lifecycle.service.js.map +1 -1
  18. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  19. package/dist/services/worker-cleanup.service.js +30 -7
  20. package/dist/services/worker-cleanup.service.js.map +1 -1
  21. package/dist/sessions.d.ts +5 -0
  22. package/dist/sessions.d.ts.map +1 -1
  23. package/dist/sessions.js +7 -0
  24. package/dist/sessions.js.map +1 -1
  25. package/dist/stdio-server.d.ts.map +1 -1
  26. package/dist/stdio-server.js +7 -1
  27. package/dist/stdio-server.js.map +1 -1
  28. package/dist/tools/delegate-tool.d.ts.map +1 -1
  29. package/dist/tools/delegate-tool.js +1 -0
  30. package/dist/tools/delegate-tool.js.map +1 -1
  31. package/dist/tools/start-session-tool.d.ts.map +1 -1
  32. package/dist/tools/start-session-tool.js +10 -19
  33. package/dist/tools/start-session-tool.js.map +1 -1
  34. package/package.json +1 -1
  35. package/supervisor/config.go +1 -1
  36. package/supervisor/health.go +19 -45
  37. package/supervisor/health_test.go +0 -29
  38. package/supervisor/keeper.go +27 -26
  39. package/supervisor/lock.go +1 -0
  40. package/supervisor/main.go +0 -4
  41. package/supervisor/service_windows.go +98 -120
  42. package/supervisor/updater.go +36 -11
@@ -114,6 +114,7 @@ func (k *Keeper) run(ctx context.Context) {
114
114
  k.log.Warn("Thread %d (worker %d) is stuck (no heartbeat for %v) — restarting", k.cfg.ThreadID, activeThreadID, k.global.StuckThreshold)
115
115
  // Kill via MCP API, then fall through to restart
116
116
  k.killThread(ctx, activeThreadID)
117
+ retryCount = 0
117
118
  activeThreadID = k.cfg.ThreadID // reset — will get new worker ID on restart
118
119
  } else {
119
120
  // Healthy — reset counters
@@ -129,7 +130,31 @@ func (k *Keeper) run(ctx context.Context) {
129
130
  }
130
131
  }
131
132
 
132
- // Thread is not running (or was stuck and killed)
133
+ // Thread is not running (or was stuck and killed).
134
+ // Detect fast-exit: if the previous successful start was recent, the thread exited too quickly.
135
+ if !lastStartTime.IsZero() && time.Since(lastStartTime) < k.global.FastExitThreshold {
136
+ fastExitCount++
137
+ if fastExitCount >= k.global.FastExitMaxCount {
138
+ cooldown := time.Duration(float64(k.global.FastExitBaseCooldown) * math.Pow(2, float64(fastExitEscalation)))
139
+ if cooldown > k.global.FastExitMaxCooldown {
140
+ cooldown = k.global.FastExitMaxCooldown
141
+ }
142
+ k.log.Warn("Thread %d: %d consecutive fast exits — backing off %v", k.cfg.ThreadID, fastExitCount, cooldown)
143
+ if k.onDeath != nil {
144
+ k.onDeath(k.cfg.ThreadID, k.cfg.SessionName+" (repeated fast exits — check credits/API key)")
145
+ }
146
+ fastExitEscalation++
147
+ k.sleep(ctx, cooldown)
148
+ fastExitCount = 0
149
+ retryCount = 0
150
+ return
151
+ }
152
+ } else if !lastStartTime.IsZero() {
153
+ // Previous start was long ago — not a fast exit pattern, reset counters
154
+ fastExitCount = 0
155
+ fastExitEscalation = 0
156
+ }
157
+
133
158
  if retryCount >= k.cfg.MaxRetries {
134
159
  cooldown := k.global.KeeperCooldown
135
160
  if k.cfg.CooldownMs > 0 {
@@ -157,10 +182,10 @@ func (k *Keeper) run(ctx context.Context) {
157
182
  return
158
183
  }
159
184
 
160
- lastStartTime = time.Now()
161
185
  ok, workerID := k.callStartThread(ctx)
162
186
 
163
187
  if ok {
188
+ lastStartTime = time.Now()
164
189
  if workerID > 0 {
165
190
  activeThreadID = workerID
166
191
  k.log.Info("Thread %d start_thread succeeded (worker %d)", k.cfg.ThreadID, workerID)
@@ -168,31 +193,7 @@ func (k *Keeper) run(ctx context.Context) {
168
193
  k.log.Info("Thread %d start_thread succeeded", k.cfg.ThreadID)
169
194
  }
170
195
  retryCount = 0
171
- // Check for fast exit on next check
172
196
  } else {
173
- // Check for fast exit pattern
174
- if !lastStartTime.IsZero() && time.Since(lastStartTime) < k.global.FastExitThreshold {
175
- fastExitCount++
176
- if fastExitCount >= k.global.FastExitMaxCount {
177
- cooldown := time.Duration(float64(k.global.FastExitBaseCooldown) * math.Pow(2, float64(fastExitEscalation)))
178
- if cooldown > k.global.FastExitMaxCooldown {
179
- cooldown = k.global.FastExitMaxCooldown
180
- }
181
- k.log.Warn("Thread %d: %d consecutive fast exits — backing off %v", k.cfg.ThreadID, fastExitCount, cooldown)
182
- if k.onDeath != nil {
183
- k.onDeath(k.cfg.ThreadID, k.cfg.SessionName+" (repeated fast exits — check credits/API key)")
184
- }
185
- fastExitEscalation++
186
- k.sleep(ctx, cooldown)
187
- fastExitCount = 0
188
- retryCount = 0
189
- return
190
- }
191
- } else {
192
- fastExitCount = 0
193
- fastExitEscalation = 0
194
- }
195
-
196
197
  retryCount++
197
198
  delay := k.backoff(retryCount)
198
199
  k.log.Info("Backing off %v before next attempt", delay)
@@ -47,6 +47,7 @@ func AcquireLock(lockPath string, log *Logger) bool {
47
47
  }
48
48
  fmt.Fprintf(f, "%d", os.Getpid())
49
49
  f.Close()
50
+ log.Info("Lock acquired: %s (PID %d)", lockPath, os.Getpid())
50
51
  return true
51
52
  }
52
53
 
@@ -395,10 +395,6 @@ func fetchKeeperSettings(ctx context.Context, mcp *MCPClient, log *Logger) ([]Ke
395
395
  continue
396
396
  }
397
397
 
398
- if typ, _ := r["type"].(string); typ == "worker" {
399
- continue
400
- }
401
-
402
398
  // Skip non-active roots (archived, expired, exited)
403
399
  if status, _ := r["status"].(string); status != "" && status != "active" {
404
400
  continue
@@ -5,6 +5,7 @@ package main
5
5
  import (
6
6
  "fmt"
7
7
  "os"
8
+ "path/filepath"
8
9
  "time"
9
10
 
10
11
  "golang.org/x/sys/windows/svc"
@@ -58,159 +59,136 @@ func runAsService() error {
58
59
  return svc.Run(serviceName, &supervisorService{})
59
60
  }
60
61
 
61
- func installService(exePath, serviceUser, servicePassword string) error {
62
+ func withServiceManager(fn func(*mgr.Mgr) error) error {
62
63
  m, err := mgr.Connect()
63
64
  if err != nil {
64
- return fmt.Errorf("install failed: connect to service manager: %w", err)
65
+ return err
65
66
  }
66
67
  defer m.Disconnect()
68
+ return fn(m)
69
+ }
67
70
 
68
- s, err := m.OpenService(serviceName)
69
- if err == nil {
70
- s.Close()
71
- return fmt.Errorf("install failed: service %q already exists", serviceName)
72
- }
71
+ func installService(exePath, serviceUser, servicePassword string) error {
72
+ return withServiceManager(func(m *mgr.Mgr) error {
73
+ s, err := m.OpenService(serviceName)
74
+ if err == nil {
75
+ s.Close()
76
+ return fmt.Errorf("install failed: service %q already exists", serviceName)
77
+ }
73
78
 
74
- cfg := mgr.Config{
75
- DisplayName: serviceDisplay,
76
- Description: serviceDesc,
77
- StartType: mgr.StartAutomatic,
78
- DelayedAutoStart: true,
79
- }
80
- if serviceUser != "" {
81
- cfg.ServiceStartName = serviceUser
82
- cfg.Password = servicePassword
83
- if servicePassword == "" {
84
- fmt.Printf("Installing service as passwordless identity %q\n", serviceUser)
79
+ cfg := mgr.Config{
80
+ DisplayName: serviceDisplay,
81
+ Description: serviceDesc,
82
+ StartType: mgr.StartAutomatic,
83
+ DelayedAutoStart: true,
84
+ }
85
+ if serviceUser != "" {
86
+ cfg.ServiceStartName = serviceUser
87
+ cfg.Password = servicePassword
88
+ if servicePassword == "" {
89
+ fmt.Printf("Installing service as passwordless identity %q\n", serviceUser)
90
+ } else {
91
+ fmt.Printf("Installing service as user %q\n", serviceUser)
92
+ }
85
93
  } else {
86
- fmt.Printf("Installing service as user %q\n", serviceUser)
94
+ fmt.Println("Installing service as LocalSystem (default). Use -service-user to run as a specific user account.")
87
95
  }
88
- } else {
89
- fmt.Println("Installing service as LocalSystem (default). Use -service-user to run as a specific user account.")
90
- }
91
96
 
92
- s, err = m.CreateService(serviceName, exePath, cfg)
93
- if err != nil {
94
- return fmt.Errorf("install failed: create service: %w", err)
95
- }
96
- defer s.Close()
97
+ s, err = m.CreateService(serviceName, exePath, cfg)
98
+ if err != nil {
99
+ return fmt.Errorf("install failed: create service: %w", err)
100
+ }
101
+ defer s.Close()
97
102
 
98
- fmt.Printf("Service %q installed successfully.\n", serviceName)
99
- fmt.Printf("Start it with: %s start\n", filepathBase(exePath))
100
- return nil
103
+ fmt.Printf("Service %q installed successfully.\n", serviceName)
104
+ fmt.Printf("Start it with: %s start\n", filepath.Base(exePath))
105
+ return nil
106
+ })
101
107
  }
102
108
 
103
109
  func uninstallService() error {
104
- m, err := mgr.Connect()
105
- if err != nil {
106
- return fmt.Errorf("uninstall failed: connect to service manager: %w", err)
107
- }
108
- defer m.Disconnect()
109
-
110
- s, err := m.OpenService(serviceName)
111
- if err != nil {
112
- return fmt.Errorf("uninstall failed: service %q not found: %w", serviceName, err)
113
- }
114
- defer s.Close()
110
+ return withServiceManager(func(m *mgr.Mgr) error {
111
+ s, err := m.OpenService(serviceName)
112
+ if err != nil {
113
+ return fmt.Errorf("uninstall failed: service %q not found: %w", serviceName, err)
114
+ }
115
+ defer s.Close()
115
116
 
116
- if err := s.Delete(); err != nil {
117
- return fmt.Errorf("uninstall failed: delete service: %w", err)
118
- }
117
+ if err := s.Delete(); err != nil {
118
+ return fmt.Errorf("uninstall failed: delete service: %w", err)
119
+ }
119
120
 
120
- fmt.Printf("Service %q uninstalled.\n", serviceName)
121
- return nil
121
+ fmt.Printf("Service %q uninstalled.\n", serviceName)
122
+ return nil
123
+ })
122
124
  }
123
125
 
124
126
  func startService() error {
125
- m, err := mgr.Connect()
126
- if err != nil {
127
- return fmt.Errorf("start failed: connect to service manager: %w", err)
128
- }
129
- defer m.Disconnect()
130
-
131
- s, err := m.OpenService(serviceName)
132
- if err != nil {
133
- return fmt.Errorf("start failed: service %q not found: %w", serviceName, err)
134
- }
135
- defer s.Close()
127
+ return withServiceManager(func(m *mgr.Mgr) error {
128
+ s, err := m.OpenService(serviceName)
129
+ if err != nil {
130
+ return fmt.Errorf("start failed: service %q not found: %w", serviceName, err)
131
+ }
132
+ defer s.Close()
136
133
 
137
- if err := s.Start(); err != nil {
138
- return fmt.Errorf("start failed: %w", err)
139
- }
134
+ if err := s.Start(); err != nil {
135
+ return fmt.Errorf("start failed: %w", err)
136
+ }
140
137
 
141
- fmt.Printf("Service %q started.\n", serviceName)
142
- return nil
138
+ fmt.Printf("Service %q started.\n", serviceName)
139
+ return nil
140
+ })
143
141
  }
144
142
 
145
143
  func stopService() error {
146
- m, err := mgr.Connect()
147
- if err != nil {
148
- return fmt.Errorf("stop failed: connect to service manager: %w", err)
149
- }
150
- defer m.Disconnect()
151
-
152
- s, err := m.OpenService(serviceName)
153
- if err != nil {
154
- return fmt.Errorf("stop failed: service %q not found: %w", serviceName, err)
155
- }
156
- defer s.Close()
144
+ return withServiceManager(func(m *mgr.Mgr) error {
145
+ s, err := m.OpenService(serviceName)
146
+ if err != nil {
147
+ return fmt.Errorf("stop failed: service %q not found: %w", serviceName, err)
148
+ }
149
+ defer s.Close()
157
150
 
158
- if _, err := s.Control(svc.Stop); err != nil {
159
- return fmt.Errorf("stop failed: %w", err)
160
- }
151
+ if _, err := s.Control(svc.Stop); err != nil {
152
+ return fmt.Errorf("stop failed: %w", err)
153
+ }
161
154
 
162
- fmt.Printf("Service %q stopping.\n", serviceName)
163
- return nil
155
+ fmt.Printf("Service %q stopping.\n", serviceName)
156
+ return nil
157
+ })
164
158
  }
165
159
 
166
160
  func serviceStatus() error {
167
- m, err := mgr.Connect()
168
- if err != nil {
169
- return fmt.Errorf("status failed: connect to service manager: %w", err)
170
- }
171
- defer m.Disconnect()
172
-
173
- s, err := m.OpenService(serviceName)
174
- if err != nil {
175
- return fmt.Errorf("status failed: service %q not found: %w", serviceName, err)
176
- }
177
- defer s.Close()
161
+ return withServiceManager(func(m *mgr.Mgr) error {
162
+ s, err := m.OpenService(serviceName)
163
+ if err != nil {
164
+ return fmt.Errorf("status failed: service %q not found: %w", serviceName, err)
165
+ }
166
+ defer s.Close()
178
167
 
179
- st, err := s.Query()
180
- if err != nil {
181
- return fmt.Errorf("status failed: query service: %w", err)
182
- }
168
+ st, err := s.Query()
169
+ if err != nil {
170
+ return fmt.Errorf("status failed: query service: %w", err)
171
+ }
183
172
 
184
- states := map[svc.State]string{
185
- svc.Stopped: "Stopped",
186
- svc.StartPending: "StartPending",
187
- svc.StopPending: "StopPending",
188
- svc.Running: "Running",
189
- svc.ContinuePending: "ContinuePending",
190
- svc.PausePending: "PausePending",
191
- svc.Paused: "Paused",
192
- }
193
- state, ok := states[st.State]
194
- if !ok {
195
- state = fmt.Sprintf("Unknown(%d)", st.State)
196
- }
173
+ states := map[svc.State]string{
174
+ svc.Stopped: "Stopped",
175
+ svc.StartPending: "StartPending",
176
+ svc.StopPending: "StopPending",
177
+ svc.Running: "Running",
178
+ svc.ContinuePending: "ContinuePending",
179
+ svc.PausePending: "PausePending",
180
+ svc.Paused: "Paused",
181
+ }
182
+ state, ok := states[st.State]
183
+ if !ok {
184
+ state = fmt.Sprintf("Unknown(%d)", st.State)
185
+ }
197
186
 
198
- fmt.Printf("Service %q: %s\n", serviceName, state)
199
- return nil
187
+ fmt.Printf("Service %q: %s\n", serviceName, state)
188
+ return nil
189
+ })
200
190
  }
201
191
 
202
192
  func isWindowsService() (bool, error) {
203
193
  return svc.IsWindowsService()
204
194
  }
205
-
206
- func filepathBase(path string) string {
207
- if path == "" {
208
- return serviceName
209
- }
210
- for i := len(path) - 1; i >= 0; i-- {
211
- if path[i] == '\\' || path[i] == '/' {
212
- return path[i+1:]
213
- }
214
- }
215
- return path
216
- }
@@ -7,6 +7,7 @@ import (
7
7
  "io"
8
8
  "net/http"
9
9
  "os"
10
+ "os/exec"
10
11
  "path/filepath"
11
12
  "runtime"
12
13
  "strings"
@@ -432,6 +433,9 @@ func (u *Updater) checkSupervisorUpdate(ctx context.Context) {
432
433
  u.state.Transition(updateScopeSupervisor, updatePhaseStaged, remote, local, "")
433
434
  notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: downloaded %s. Restarting supervisor to apply update...", remote), 0)
434
435
 
436
+ // Reset start time so minimum uptime is re-enforced after restart
437
+ u.startAt = time.Now()
438
+
435
439
  isService, err := isWindowsService()
436
440
  if err != nil {
437
441
  markFailed(err)
@@ -450,7 +454,7 @@ func (u *Updater) checkSupervisorUpdate(ctx context.Context) {
450
454
  return
451
455
  }
452
456
 
453
- if err := signalSelf(syscall.SIGTERM); err != nil {
457
+ if err := requestSupervisorRestart(u.log); err != nil {
454
458
  markFailed(err)
455
459
  u.log.Error("Failed to signal supervisor for restart: %v", err)
456
460
  notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but restart signal failed.", 0)
@@ -498,14 +502,6 @@ func (u *Updater) downloadSupervisorBinary(ctx context.Context, downloadURL stri
498
502
  return fmt.Errorf("downloaded empty binary")
499
503
  }
500
504
 
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
505
  if err := os.Remove(u.cfg.Paths.PendingBinary); err != nil && !os.IsNotExist(err) {
510
506
  return err
511
507
  }
@@ -513,7 +509,7 @@ func (u *Updater) downloadSupervisorBinary(ctx context.Context, downloadURL stri
513
509
  return err
514
510
  }
515
511
 
516
- u.log.Info("Supervisor binary downloaded to %s (%d bytes)", u.cfg.Paths.PendingBinary, info.Size())
512
+ u.log.Info("Supervisor binary downloaded to %s (%d bytes)", u.cfg.Paths.PendingBinary, written)
517
513
  return nil
518
514
  }
519
515
 
@@ -525,6 +521,35 @@ func signalSelf(sig os.Signal) error {
525
521
  return proc.Signal(sig)
526
522
  }
527
523
 
524
+ func requestSupervisorRestart(log *Logger) error {
525
+ if runtime.GOOS != "windows" {
526
+ return signalSelf(syscall.SIGTERM)
527
+ }
528
+
529
+ exePath, err := os.Executable()
530
+ if err != nil {
531
+ return fmt.Errorf("resolve executable path: %w", err)
532
+ }
533
+
534
+ cmd := exec.Command(exePath)
535
+ cmd.Env = os.Environ()
536
+ setSysProcAttr(cmd)
537
+ if err := cmd.Start(); err != nil {
538
+ return fmt.Errorf("start replacement supervisor: %w", err)
539
+ }
540
+
541
+ if log != nil {
542
+ log.Info("Spawned replacement supervisor process PID %d", cmd.Process.Pid)
543
+ }
544
+
545
+ go func() {
546
+ time.Sleep(2 * time.Second)
547
+ os.Exit(0)
548
+ }()
549
+
550
+ return nil
551
+ }
552
+
528
553
  func (u *Updater) killServer() {
529
554
  u.log.Info("Updater: stopping current MCP server for update")
530
555
  pid, err := ReadPIDFile(u.cfg.Paths.ServerPID)
@@ -570,7 +595,7 @@ func (u *Updater) clearNpxCache() {
570
595
  }
571
596
  pkgDir := filepath.Join(base, e.Name(), "node_modules", "sensorium-mcp")
572
597
  // Validate path doesn't escape base directory
573
- if !strings.HasPrefix(pkgDir, base) {
598
+ if !strings.HasPrefix(pkgDir, base+string(os.PathSeparator)) {
574
599
  continue
575
600
  }
576
601
  if _, err := os.Stat(pkgDir); err == nil {