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
@@ -0,0 +1,216 @@
1
+ //go:build windows
2
+
3
+ package main
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "time"
9
+
10
+ "golang.org/x/sys/windows/svc"
11
+ "golang.org/x/sys/windows/svc/mgr"
12
+ )
13
+
14
+ const serviceName = "SensoriumSupervisor"
15
+ const serviceDisplay = "Sensorium Supervisor"
16
+ const serviceDesc = "Keeps the sensorium-mcp server and agent threads running."
17
+
18
+ type supervisorService struct{}
19
+
20
+ func (s *supervisorService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
21
+ status <- svc.Status{State: svc.StartPending}
22
+
23
+ done := make(chan error, 1)
24
+ go func() {
25
+ done <- runSupervisor(true)
26
+ }()
27
+
28
+ status <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
29
+
30
+ for {
31
+ select {
32
+ case c := <-r:
33
+ switch c.Cmd {
34
+ case svc.Interrogate:
35
+ status <- c.CurrentStatus
36
+ case svc.Stop, svc.Shutdown:
37
+ status <- svc.Status{State: svc.StopPending}
38
+ stopSupervisor()
39
+ select {
40
+ case err := <-done:
41
+ if err != nil {
42
+ fmt.Fprintf(os.Stderr, "Service shutdown with error: %v\n", err)
43
+ }
44
+ case <-time.After(15 * time.Second):
45
+ }
46
+ return false, 0
47
+ }
48
+ case err := <-done:
49
+ if err != nil {
50
+ fmt.Fprintf(os.Stderr, "Service failed: %v\n", err)
51
+ }
52
+ return false, 0
53
+ }
54
+ }
55
+ }
56
+
57
+ func runAsService() error {
58
+ return svc.Run(serviceName, &supervisorService{})
59
+ }
60
+
61
+ func installService(exePath, serviceUser, servicePassword string) error {
62
+ m, err := mgr.Connect()
63
+ if err != nil {
64
+ return fmt.Errorf("install failed: connect to service manager: %w", err)
65
+ }
66
+ defer m.Disconnect()
67
+
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
+ }
73
+
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)
85
+ } else {
86
+ fmt.Printf("Installing service as user %q\n", serviceUser)
87
+ }
88
+ } else {
89
+ fmt.Println("Installing service as LocalSystem (default). Use -service-user to run as a specific user account.")
90
+ }
91
+
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
+
98
+ fmt.Printf("Service %q installed successfully.\n", serviceName)
99
+ fmt.Printf("Start it with: %s start\n", filepathBase(exePath))
100
+ return nil
101
+ }
102
+
103
+ 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()
115
+
116
+ if err := s.Delete(); err != nil {
117
+ return fmt.Errorf("uninstall failed: delete service: %w", err)
118
+ }
119
+
120
+ fmt.Printf("Service %q uninstalled.\n", serviceName)
121
+ return nil
122
+ }
123
+
124
+ 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()
136
+
137
+ if err := s.Start(); err != nil {
138
+ return fmt.Errorf("start failed: %w", err)
139
+ }
140
+
141
+ fmt.Printf("Service %q started.\n", serviceName)
142
+ return nil
143
+ }
144
+
145
+ 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()
157
+
158
+ if _, err := s.Control(svc.Stop); err != nil {
159
+ return fmt.Errorf("stop failed: %w", err)
160
+ }
161
+
162
+ fmt.Printf("Service %q stopping.\n", serviceName)
163
+ return nil
164
+ }
165
+
166
+ 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()
178
+
179
+ st, err := s.Query()
180
+ if err != nil {
181
+ return fmt.Errorf("status failed: query service: %w", err)
182
+ }
183
+
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
+ }
197
+
198
+ fmt.Printf("Service %q: %s\n", serviceName, state)
199
+ return nil
200
+ }
201
+
202
+ func isWindowsService() (bool, error) {
203
+ return svc.IsWindowsService()
204
+ }
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
+ }
@@ -0,0 +1,264 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
10
+ "time"
11
+ )
12
+
13
+ const (
14
+ updateScopeMCP = "mcp"
15
+ updateScopeSupervisor = "supervisor"
16
+
17
+ // A coordinator lock older than this is treated as stale metadata for
18
+ // diagnostics, but age never overrides live-owner safety.
19
+ updateCoordinatorLockMaxAge = 10 * time.Minute
20
+
21
+ updatePhaseIdle = "idle"
22
+ updatePhaseStaged = "staged"
23
+ updatePhaseApplying = "applying"
24
+ updatePhaseRestarting = "restarting"
25
+ updatePhaseVerifying = "verifying"
26
+ updatePhaseRollback = "rollback"
27
+ updatePhaseFailed = "failed"
28
+ )
29
+
30
+ type UpdateState struct {
31
+ Scope string `json:"scope"`
32
+ Phase string `json:"phase"`
33
+ TargetVersion string `json:"targetVersion"`
34
+ PreviousVersion string `json:"previousVersion"`
35
+ UpdatedAt time.Time `json:"updatedAt"`
36
+ LastError string `json:"lastError"`
37
+ }
38
+
39
+ type UpdateStateStore struct {
40
+ path string
41
+ log *Logger
42
+ }
43
+
44
+ func NewUpdateStateStore(path string, log *Logger) *UpdateStateStore {
45
+ return &UpdateStateStore{path: path, log: log}
46
+ }
47
+
48
+ func (s *UpdateStateStore) Load() (UpdateState, error) {
49
+ data, err := os.ReadFile(s.path)
50
+ if err != nil {
51
+ if errors.Is(err, os.ErrNotExist) {
52
+ return UpdateState{Phase: updatePhaseIdle, UpdatedAt: time.Now().UTC()}, nil
53
+ }
54
+ return UpdateState{}, err
55
+ }
56
+
57
+ var state UpdateState
58
+ if err := json.Unmarshal(data, &state); err != nil {
59
+ return UpdateState{}, err
60
+ }
61
+ if state.Phase == "" {
62
+ state.Phase = updatePhaseIdle
63
+ }
64
+ return state, nil
65
+ }
66
+
67
+ func (s *UpdateStateStore) Save(state UpdateState) error {
68
+ if state.UpdatedAt.IsZero() {
69
+ state.UpdatedAt = time.Now().UTC()
70
+ }
71
+
72
+ if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
73
+ return fmt.Errorf("create update state dir: %w", err)
74
+ }
75
+
76
+ data, err := json.Marshal(state)
77
+ if err != nil {
78
+ return fmt.Errorf("marshal update state: %w", err)
79
+ }
80
+ if err := atomicWrite(s.path, data); err != nil {
81
+ return fmt.Errorf("write update state: %w", err)
82
+ }
83
+ return nil
84
+ }
85
+
86
+ func (s *UpdateStateStore) Transition(scope, phase, targetVersion, previousVersion, lastError string) {
87
+ state := UpdateState{
88
+ Scope: scope,
89
+ Phase: phase,
90
+ TargetVersion: targetVersion,
91
+ PreviousVersion: previousVersion,
92
+ UpdatedAt: time.Now().UTC(),
93
+ LastError: lastError,
94
+ }
95
+ if err := s.Save(state); err != nil {
96
+ s.log.Warn("Failed to persist update state (scope=%s phase=%s): %v", scope, phase, err)
97
+ }
98
+ }
99
+
100
+ type updateLockOwner struct {
101
+ Scope string `json:"scope"`
102
+ PID int `json:"pid"`
103
+ UpdatedAt time.Time `json:"updatedAt"`
104
+ }
105
+
106
+ type UpdateCoordinatorLock struct {
107
+ path string
108
+ scope string
109
+ log *Logger
110
+ }
111
+
112
+ type updateLockFile interface {
113
+ Write([]byte) (int, error)
114
+ Close() error
115
+ }
116
+
117
+ var openUpdateLockFile = func(lockPath string) (updateLockFile, error) {
118
+ return os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
119
+ }
120
+
121
+ func writeUpdateLockMetadata(lockPath string, payload []byte) error {
122
+ f, err := openUpdateLockFile(lockPath)
123
+ if err != nil {
124
+ return err
125
+ }
126
+
127
+ if _, err := f.Write(payload); err != nil {
128
+ closeErr := f.Close()
129
+ _ = os.Remove(lockPath)
130
+ if closeErr != nil {
131
+ return errors.Join(fmt.Errorf("write lock metadata: %w", err), fmt.Errorf("close lock file after write failure: %w", closeErr))
132
+ }
133
+ return fmt.Errorf("write lock metadata: %w", err)
134
+ }
135
+
136
+ if err := f.Close(); err != nil {
137
+ _ = os.Remove(lockPath)
138
+ return fmt.Errorf("close lock metadata file: %w", err)
139
+ }
140
+
141
+ return nil
142
+ }
143
+
144
+ func AcquireUpdateCoordinatorLock(lockPath, scope string, log *Logger) (*UpdateCoordinatorLock, bool) {
145
+ owner := updateLockOwner{Scope: scope, PID: os.Getpid(), UpdatedAt: time.Now().UTC()}
146
+ payload, _ := json.Marshal(owner)
147
+
148
+ err := writeUpdateLockMetadata(lockPath, payload)
149
+ if err == nil {
150
+ log.Debug("Update coordinator lock acquired by %s", scope)
151
+ return &UpdateCoordinatorLock{path: lockPath, scope: scope, log: log}, true
152
+ }
153
+
154
+ if !errors.Is(err, os.ErrExist) {
155
+ log.Warn("Failed to acquire update coordinator lock %s: %v", lockPath, err)
156
+ return nil, false
157
+ }
158
+
159
+ data, readErr := os.ReadFile(lockPath)
160
+ if readErr != nil {
161
+ log.Warn("Update coordinator lock exists but could not be read (%s): %v", lockPath, readErr)
162
+ return nil, false
163
+ }
164
+
165
+ var holder updateLockOwner
166
+ if json.Unmarshal(data, &holder) == nil {
167
+ lockAge := time.Since(holder.UpdatedAt)
168
+ alive := holder.PID > 0 && IsProcessAlive(holder.PID)
169
+ staleByAge := !holder.UpdatedAt.IsZero() && lockAge > updateCoordinatorLockMaxAge
170
+
171
+ if alive {
172
+ holderScope := holder.Scope
173
+ if holderScope == "" {
174
+ holderScope = "unknown"
175
+ }
176
+ ageNote := ""
177
+ if staleByAge {
178
+ ageNote = fmt.Sprintf("; stale metadata age %v exceeds %v", lockAge.Round(time.Second), updateCoordinatorLockMaxAge)
179
+ }
180
+ log.Info("Skipping %s update apply: coordinator lock held by %s (PID %d, age %v%s)", scope, holderScope, holder.PID, lockAge.Round(time.Second), ageNote)
181
+ return nil, false
182
+ }
183
+
184
+ holderScope := holder.Scope
185
+ if holderScope == "" {
186
+ holderScope = "unknown"
187
+ }
188
+ reason := "owner PID not alive"
189
+ if staleByAge {
190
+ reason = fmt.Sprintf("owner PID not alive (metadata age %v exceeds %v)", lockAge.Round(time.Second), updateCoordinatorLockMaxAge)
191
+ }
192
+ log.Warn("Reclaiming update coordinator lock for %s: previous owner=%s pid=%d (%s)", scope, holderScope, holder.PID, reason)
193
+ }
194
+
195
+ _ = os.Remove(lockPath)
196
+ err = writeUpdateLockMetadata(lockPath, payload)
197
+ if err != nil {
198
+ log.Warn("Failed to reclaim update coordinator lock %s: %v", lockPath, err)
199
+ return nil, false
200
+ }
201
+ log.Warn("Reclaimed stale update coordinator lock for %s", scope)
202
+ return &UpdateCoordinatorLock{path: lockPath, scope: scope, log: log}, true
203
+ }
204
+
205
+ func (l *UpdateCoordinatorLock) Release() {
206
+ if l == nil {
207
+ return
208
+ }
209
+ _ = os.Remove(l.path)
210
+ l.log.Debug("Update coordinator lock released by %s", l.scope)
211
+ }
212
+
213
+ func recoverPersistedUpdateStateOnStartup(cfg Config, log *Logger) {
214
+ store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
215
+ state, err := store.Load()
216
+ if err != nil {
217
+ log.Warn("Failed to load persisted update state for startup recovery: %v", err)
218
+ return
219
+ }
220
+
221
+ scope := state.Scope
222
+ if scope == "" {
223
+ scope = updateScopeMCP
224
+ }
225
+
226
+ if scope == updateScopeSupervisor {
227
+ currentVersion := readTrimmedFile(cfg.Paths.SupervisorVersion)
228
+ targetVersion := strings.TrimSpace(state.TargetVersion)
229
+ if targetVersion != "" && currentVersion == targetVersion && state.Phase != updatePhaseIdle {
230
+ log.Info("Startup recovery: supervisor update %s already applied locally; transitioning state to idle", targetVersion)
231
+ store.Transition(updateScopeSupervisor, updatePhaseIdle, targetVersion, state.PreviousVersion, "")
232
+ return
233
+ }
234
+ }
235
+
236
+ if state.Phase == "" || state.Phase == updatePhaseIdle || state.Phase == updatePhaseFailed {
237
+ return
238
+ }
239
+
240
+ if !isRecoverableStartupPhase(state.Phase) {
241
+ return
242
+ }
243
+
244
+ reason := fmt.Sprintf("startup recovery: stale non-idle update state detected (%s/%s)", scope, state.Phase)
245
+ log.Warn("%s", reason)
246
+ store.Transition(scope, updatePhaseFailed, state.TargetVersion, state.PreviousVersion, reason)
247
+ }
248
+
249
+ func isRecoverableStartupPhase(phase string) bool {
250
+ switch phase {
251
+ case updatePhaseApplying, updatePhaseRestarting, updatePhaseVerifying, updatePhaseStaged:
252
+ return true
253
+ default:
254
+ return false
255
+ }
256
+ }
257
+
258
+ func readTrimmedFile(path string) string {
259
+ data, err := os.ReadFile(path)
260
+ if err != nil {
261
+ return ""
262
+ }
263
+ return strings.TrimSpace(string(data))
264
+ }