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.
- package/Install-Sensorium.ps1 +351 -0
- package/README.md +14 -0
- package/dist/config.d.ts +16 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -2
- package/dist/config.js.map +1 -1
- package/dist/daily-session.d.ts +2 -1
- package/dist/daily-session.d.ts.map +1 -1
- package/dist/daily-session.js +23 -26
- package/dist/daily-session.js.map +1 -1
- package/dist/dashboard/routes/settings.d.ts +4 -0
- package/dist/dashboard/routes/settings.d.ts.map +1 -1
- package/dist/dashboard/routes/settings.js +57 -1
- package/dist/dashboard/routes/settings.js.map +1 -1
- package/dist/dashboard/routes/threads.d.ts +1 -0
- package/dist/dashboard/routes/threads.d.ts.map +1 -1
- package/dist/dashboard/routes/threads.js +23 -25
- package/dist/dashboard/routes/threads.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +7 -2
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/spa.html +11 -11
- package/dist/data/interfaces.d.ts +36 -0
- package/dist/data/interfaces.d.ts.map +1 -0
- package/dist/data/interfaces.js +2 -0
- package/dist/data/interfaces.js.map +1 -0
- package/dist/data/memory/bootstrap.d.ts +36 -16
- package/dist/data/memory/bootstrap.d.ts.map +1 -1
- package/dist/data/memory/bootstrap.js +71 -217
- package/dist/data/memory/bootstrap.js.map +1 -1
- package/dist/data/memory/consolidation.d.ts +35 -34
- package/dist/data/memory/consolidation.d.ts.map +1 -1
- package/dist/data/memory/consolidation.js +43 -554
- package/dist/data/memory/consolidation.js.map +1 -1
- package/dist/data/memory/migration-runner.d.ts +5 -0
- package/dist/data/memory/migration-runner.d.ts.map +1 -0
- package/dist/data/memory/migration-runner.js +403 -0
- package/dist/data/memory/migration-runner.js.map +1 -0
- package/dist/data/memory/reflection.js +1 -1
- package/dist/data/memory/schema-ddl.d.ts +4 -0
- package/dist/data/memory/schema-ddl.d.ts.map +1 -0
- package/dist/data/memory/schema-ddl.js +194 -0
- package/dist/data/memory/schema-ddl.js.map +1 -0
- package/dist/data/memory/schema-guard.d.ts +3 -0
- package/dist/data/memory/schema-guard.d.ts.map +1 -0
- package/dist/data/memory/schema-guard.js +184 -0
- package/dist/data/memory/schema-guard.js.map +1 -0
- package/dist/data/memory/schema.d.ts +2 -5
- package/dist/data/memory/schema.d.ts.map +1 -1
- package/dist/data/memory/schema.js +6 -834
- package/dist/data/memory/schema.js.map +1 -1
- package/dist/data/memory/synthesis.js +2 -2
- package/dist/data/memory/synthesis.js.map +1 -1
- package/dist/data/memory/thread-registry.d.ts +18 -4
- package/dist/data/memory/thread-registry.d.ts.map +1 -1
- package/dist/data/memory/thread-registry.js +25 -0
- package/dist/data/memory/thread-registry.js.map +1 -1
- package/dist/data/sent-message.repository.d.ts +12 -0
- package/dist/data/sent-message.repository.d.ts.map +1 -0
- package/dist/data/sent-message.repository.js +31 -0
- package/dist/data/sent-message.repository.js.map +1 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +23 -2
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +27 -48
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +7 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +89 -12
- package/dist/logger.js.map +1 -1
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +15 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/server/factory.d.ts +2 -1
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +11 -4
- package/dist/server/factory.js.map +1 -1
- package/dist/services/agent-spawn.service.d.ts +39 -0
- package/dist/services/agent-spawn.service.d.ts.map +1 -0
- package/dist/services/agent-spawn.service.js +348 -0
- package/dist/services/agent-spawn.service.js.map +1 -0
- package/dist/services/background-runner.d.ts +26 -0
- package/dist/services/background-runner.d.ts.map +1 -0
- package/dist/services/background-runner.js +71 -0
- package/dist/services/background-runner.js.map +1 -0
- package/dist/services/consolidation.service.d.ts +16 -0
- package/dist/services/consolidation.service.d.ts.map +1 -0
- package/dist/services/consolidation.service.js +508 -0
- package/dist/services/consolidation.service.js.map +1 -0
- package/dist/services/dispatcher/broker.d.ts +2 -0
- package/dist/services/dispatcher/broker.d.ts.map +1 -1
- package/dist/services/dispatcher/broker.js +5 -10
- package/dist/services/dispatcher/broker.js.map +1 -1
- package/dist/services/dispatcher/index.d.ts +1 -1
- package/dist/services/dispatcher/index.d.ts.map +1 -1
- package/dist/services/dispatcher/index.js +1 -1
- package/dist/services/dispatcher/index.js.map +1 -1
- package/dist/services/dispatcher/lock.d.ts.map +1 -1
- package/dist/services/dispatcher/lock.js +7 -11
- package/dist/services/dispatcher/lock.js.map +1 -1
- package/dist/services/maintenance-signal.d.ts +18 -0
- package/dist/services/maintenance-signal.d.ts.map +1 -0
- package/dist/services/maintenance-signal.js +48 -0
- package/dist/services/maintenance-signal.js.map +1 -0
- package/dist/services/memory-briefing.service.d.ts +4 -0
- package/dist/services/memory-briefing.service.d.ts.map +1 -0
- package/dist/services/memory-briefing.service.js +143 -0
- package/dist/services/memory-briefing.service.js.map +1 -0
- package/dist/services/process.service.d.ts +31 -0
- package/dist/services/process.service.d.ts.map +1 -0
- package/dist/services/process.service.js +100 -0
- package/dist/services/process.service.js.map +1 -0
- package/dist/services/thread-health.service.d.ts +18 -0
- package/dist/services/thread-health.service.d.ts.map +1 -0
- package/dist/services/thread-health.service.js +118 -0
- package/dist/services/thread-health.service.js.map +1 -0
- package/dist/services/thread-lifecycle.service.d.ts +52 -0
- package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
- package/dist/services/thread-lifecycle.service.js +174 -0
- package/dist/services/thread-lifecycle.service.js.map +1 -0
- package/dist/services/topic.service.d.ts +25 -0
- package/dist/services/topic.service.d.ts.map +1 -0
- package/dist/services/topic.service.js +65 -0
- package/dist/services/topic.service.js.map +1 -0
- package/dist/services/worker-cleanup.service.d.ts +8 -0
- package/dist/services/worker-cleanup.service.d.ts.map +1 -0
- package/dist/services/worker-cleanup.service.js +82 -0
- package/dist/services/worker-cleanup.service.js.map +1 -0
- package/dist/sessions.d.ts +14 -0
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +55 -0
- package/dist/sessions.js.map +1 -1
- package/dist/telegram.d.ts +13 -6
- package/dist/telegram.d.ts.map +1 -1
- package/dist/telegram.js +43 -14
- package/dist/telegram.js.map +1 -1
- package/dist/tools/delegate-tool.d.ts +4 -0
- package/dist/tools/delegate-tool.d.ts.map +1 -1
- package/dist/tools/delegate-tool.js +48 -109
- package/dist/tools/delegate-tool.js.map +1 -1
- package/dist/tools/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory-tools.js +1 -1
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/shared-agent-utils.d.ts +9 -1
- package/dist/tools/shared-agent-utils.d.ts.map +1 -1
- package/dist/tools/shared-agent-utils.js +21 -38
- package/dist/tools/shared-agent-utils.js.map +1 -1
- package/dist/tools/start-session-tool.d.ts +2 -0
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +68 -118
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/thread-lifecycle.d.ts +5 -127
- package/dist/tools/thread-lifecycle.d.ts.map +1 -1
- package/dist/tools/thread-lifecycle.js +5 -1167
- package/dist/tools/thread-lifecycle.js.map +1 -1
- package/dist/tools/utility-tools.js +5 -2
- package/dist/tools/utility-tools.js.map +1 -1
- package/dist/tools/wait/drive-handler.d.ts +0 -1
- package/dist/tools/wait/drive-handler.d.ts.map +1 -1
- package/dist/tools/wait/drive-handler.js +5 -22
- package/dist/tools/wait/drive-handler.js.map +1 -1
- package/dist/tools/wait/message-delivery.js +1 -1
- package/dist/tools/wait/message-delivery.js.map +1 -1
- package/dist/tools/wait/message-processing.d.ts.map +1 -1
- package/dist/tools/wait/message-processing.js +9 -8
- package/dist/tools/wait/message-processing.js.map +1 -1
- package/dist/tools/wait/poll-loop.d.ts +2 -0
- package/dist/tools/wait/poll-loop.d.ts.map +1 -1
- package/dist/tools/wait/poll-loop.js +27 -29
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/dist/tools/wait/task-handler.d.ts +0 -3
- package/dist/tools/wait/task-handler.d.ts.map +1 -1
- package/dist/tools/wait/task-handler.js +3 -2
- package/dist/tools/wait/task-handler.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -8
- package/supervisor/config.go +182 -69
- package/supervisor/config_test.go +78 -0
- package/supervisor/go.mod +12 -0
- package/supervisor/go.sum +20 -0
- package/supervisor/health.go +24 -6
- package/supervisor/keeper.go +15 -10
- package/supervisor/log.go +109 -28
- package/supervisor/log_test.go +86 -6
- package/supervisor/main.go +146 -19
- package/supervisor/main_test.go +130 -0
- package/supervisor/process.go +47 -4
- package/supervisor/process_test.go +14 -0
- package/supervisor/secrets.go +95 -0
- package/supervisor/secrets_securevault_test.go +98 -0
- package/supervisor/secrets_test.go +119 -0
- package/supervisor/self_update.go +282 -0
- package/supervisor/self_update_test.go +177 -0
- package/supervisor/service_restart_stub.go +9 -0
- package/supervisor/service_restart_windows.go +63 -0
- package/supervisor/service_stub.go +15 -0
- package/supervisor/service_windows.go +216 -0
- package/supervisor/update_state.go +264 -0
- package/supervisor/update_state_test.go +306 -0
- package/supervisor/updater.go +341 -10
- package/supervisor/updater_test.go +64 -0
- package/scripts/install-supervisor.ps1 +0 -67
- package/scripts/install-supervisor.sh +0 -43
- package/scripts/start-supervisor.ps1 +0 -46
- package/scripts/start-supervisor.sh +0 -20
package/supervisor/keeper.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"encoding/json"
|
|
6
6
|
"fmt"
|
|
7
7
|
"math"
|
|
8
|
+
"path/filepath"
|
|
8
9
|
"sync"
|
|
9
10
|
"time"
|
|
10
11
|
)
|
|
@@ -119,9 +120,11 @@ func (k *Keeper) run(ctx context.Context) {
|
|
|
119
120
|
if retryCount > 0 {
|
|
120
121
|
k.log.Info("Thread %d is healthy again (was at retry %d)", k.cfg.ThreadID, retryCount)
|
|
121
122
|
} else {
|
|
122
|
-
k.log.
|
|
123
|
+
k.log.Info("Thread %d is healthy", k.cfg.ThreadID)
|
|
123
124
|
}
|
|
124
125
|
retryCount = 0
|
|
126
|
+
fastExitCount = 0
|
|
127
|
+
fastExitEscalation = 0
|
|
125
128
|
return
|
|
126
129
|
}
|
|
127
130
|
}
|
|
@@ -243,14 +246,14 @@ func (k *Keeper) callStartThread(ctx context.Context) (bool, int) {
|
|
|
243
246
|
func (k *Keeper) killThread(ctx context.Context, threadID int) {
|
|
244
247
|
k.log.Info("Killing stuck thread %d", threadID)
|
|
245
248
|
// Read PID from thread PID file
|
|
246
|
-
pidFile := k.global.Paths.PIDsDir
|
|
249
|
+
pidFile := filepath.Join(k.global.Paths.PIDsDir, fmt.Sprintf("%d.pid", threadID))
|
|
247
250
|
pid, err := ReadPIDFile(pidFile)
|
|
248
251
|
if err != nil {
|
|
249
|
-
k.log.Warn("Cannot read PID for thread %d: %v",
|
|
252
|
+
k.log.Warn("Cannot read PID for thread %d: %v", threadID, err)
|
|
250
253
|
return
|
|
251
254
|
}
|
|
252
255
|
if err := KillProcess(pid, k.log); err != nil {
|
|
253
|
-
k.log.Error("Failed to kill thread %d (PID %d): %v",
|
|
256
|
+
k.log.Error("Failed to kill thread %d (PID %d): %v", threadID, pid, err)
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
259
|
|
|
@@ -269,14 +272,16 @@ func (k *Keeper) sleep(ctx context.Context, d time.Duration) {
|
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
274
|
|
|
272
|
-
// isRootKeepAlive checks whether the
|
|
275
|
+
// isRootKeepAlive checks whether the thread still has keepAlive=true.
|
|
276
|
+
// Uses the /api/threads/keepalive endpoint (root, branch, daily — excludes workers)
|
|
277
|
+
// so that branch and daily threads are correctly included in the check.
|
|
273
278
|
func (k *Keeper) isRootKeepAlive(ctx context.Context) bool {
|
|
274
|
-
|
|
279
|
+
threads, err := k.mcp.GetKeepAliveThreads(ctx)
|
|
275
280
|
if err != nil {
|
|
276
|
-
k.log.Debug("isRootKeepAlive(%d): failed to fetch
|
|
281
|
+
k.log.Debug("isRootKeepAlive(%d): failed to fetch keepalive threads: %v — assuming still alive", k.cfg.ThreadID, err)
|
|
277
282
|
return true // fail-open: don't stop keeper if we can't check
|
|
278
283
|
}
|
|
279
|
-
for _, r := range
|
|
284
|
+
for _, r := range threads {
|
|
280
285
|
tidFloat, _ := r["threadId"].(float64)
|
|
281
286
|
if int(tidFloat) == k.cfg.ThreadID {
|
|
282
287
|
keepAlive, _ := r["keepAlive"].(bool)
|
|
@@ -284,8 +289,8 @@ func (k *Keeper) isRootKeepAlive(ctx context.Context) bool {
|
|
|
284
289
|
return keepAlive && (status == "" || status == "active")
|
|
285
290
|
}
|
|
286
291
|
}
|
|
287
|
-
k.log.Debug("isRootKeepAlive(%d):
|
|
288
|
-
return false //
|
|
292
|
+
k.log.Debug("isRootKeepAlive(%d): thread not found in keepalive list", k.cfg.ThreadID)
|
|
293
|
+
return false // thread gone or keepAlive removed → stop keeper
|
|
289
294
|
}
|
|
290
295
|
|
|
291
296
|
// parseWorkerThreadID extracts the "threadId" field from a start_thread JSON response.
|
package/supervisor/log.go
CHANGED
|
@@ -3,30 +3,40 @@ package main
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
5
5
|
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"sort"
|
|
8
|
+
"strings"
|
|
6
9
|
"sync"
|
|
7
10
|
"time"
|
|
8
11
|
)
|
|
9
12
|
|
|
10
13
|
// Logger writes to both stderr and a rotating log file.
|
|
11
|
-
// Rotates when the file exceeds maxSize bytes.
|
|
14
|
+
// Rotates daily (at midnight) and when the file exceeds maxSize bytes.
|
|
12
15
|
type Logger struct {
|
|
13
|
-
mu
|
|
14
|
-
logPath
|
|
15
|
-
file
|
|
16
|
-
debug
|
|
17
|
-
size
|
|
18
|
-
maxSize
|
|
19
|
-
maxKeep
|
|
16
|
+
mu sync.Mutex
|
|
17
|
+
logPath string
|
|
18
|
+
file *os.File
|
|
19
|
+
debug bool
|
|
20
|
+
size int64
|
|
21
|
+
maxSize int64 // default 5 MB
|
|
22
|
+
maxKeep int // max daily rotated files to keep
|
|
23
|
+
today string
|
|
24
|
+
stopTimer chan struct{}
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
func NewLogger(logPath string) *Logger {
|
|
23
28
|
l := &Logger{
|
|
24
|
-
logPath:
|
|
25
|
-
debug:
|
|
26
|
-
maxSize:
|
|
27
|
-
maxKeep:
|
|
29
|
+
logPath: logPath,
|
|
30
|
+
debug: os.Getenv("SUPERVISOR_DEBUG") == "1" || os.Getenv("SUPERVISOR_DEBUG") == "true",
|
|
31
|
+
maxSize: 5 * 1024 * 1024, // 5 MB
|
|
32
|
+
maxKeep: 7, // keep 7 daily files
|
|
33
|
+
stopTimer: make(chan struct{}),
|
|
28
34
|
}
|
|
35
|
+
// Rotate previous day's log on startup if needed
|
|
36
|
+
l.today = time.Now().Format("2006-01-02")
|
|
37
|
+
l.rotateDailyIfNeeded()
|
|
29
38
|
l.openFile()
|
|
39
|
+
l.startMidnightTimer()
|
|
30
40
|
return l
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -53,7 +63,11 @@ func (l *Logger) log(level, format string, args ...any) {
|
|
|
53
63
|
|
|
54
64
|
l.mu.Lock()
|
|
55
65
|
defer l.mu.Unlock()
|
|
56
|
-
|
|
66
|
+
// Always write to file for post-mortem debugging; only emit DEBUG to stderr
|
|
67
|
+
// when SUPERVISOR_DEBUG is set.
|
|
68
|
+
if level != "DEBUG" || l.debug {
|
|
69
|
+
fmt.Fprint(os.Stderr, line)
|
|
70
|
+
}
|
|
57
71
|
if l.file != nil {
|
|
58
72
|
n, err := l.file.WriteString(line)
|
|
59
73
|
if err != nil {
|
|
@@ -69,30 +83,93 @@ func (l *Logger) log(level, format string, args ...any) {
|
|
|
69
83
|
func (l *Logger) Info(format string, args ...any) { l.log("INFO", format, args...) }
|
|
70
84
|
func (l *Logger) Warn(format string, args ...any) { l.log("WARN", format, args...) }
|
|
71
85
|
func (l *Logger) Error(format string, args ...any) { l.log("ERROR", format, args...) }
|
|
72
|
-
func (l *Logger) Debug(format string, args ...any) {
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
func (l *Logger) Debug(format string, args ...any) { l.log("DEBUG", format, args...) }
|
|
87
|
+
|
|
88
|
+
// rotateDailyIfNeeded renames the current log to a dated archive if it was
|
|
89
|
+
// written on a previous day. Called without mu held (used at startup and from
|
|
90
|
+
// the midnight timer before the lock is acquired).
|
|
91
|
+
func (l *Logger) rotateDailyIfNeeded() {
|
|
92
|
+
info, err := os.Stat(l.logPath)
|
|
93
|
+
if err != nil {
|
|
94
|
+
return // file doesn't exist yet — nothing to rotate
|
|
95
|
+
}
|
|
96
|
+
modDay := info.ModTime().Format("2006-01-02")
|
|
97
|
+
if modDay == l.today {
|
|
98
|
+
return // same day — no rotation needed
|
|
99
|
+
}
|
|
100
|
+
// Rename to dated file
|
|
101
|
+
ext := filepath.Ext(l.logPath)
|
|
102
|
+
base := strings.TrimSuffix(l.logPath, ext)
|
|
103
|
+
dated := fmt.Sprintf("%s.%s%s", base, modDay, ext)
|
|
104
|
+
if err := os.Rename(l.logPath, dated); err != nil {
|
|
105
|
+
fmt.Fprintf(os.Stderr, "[WARN] daily log rotate: %v\n", err)
|
|
75
106
|
}
|
|
107
|
+
l.pruneOldLogs()
|
|
76
108
|
}
|
|
77
109
|
|
|
78
|
-
//
|
|
79
|
-
|
|
110
|
+
// pruneOldLogs deletes daily log files beyond maxKeep. Called without mu held.
|
|
111
|
+
func (l *Logger) pruneOldLogs() {
|
|
112
|
+
dir := filepath.Dir(l.logPath)
|
|
113
|
+
base := strings.TrimSuffix(filepath.Base(l.logPath), filepath.Ext(l.logPath))
|
|
114
|
+
entries, err := os.ReadDir(dir)
|
|
115
|
+
if err != nil {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
var dated []string
|
|
119
|
+
for _, e := range entries {
|
|
120
|
+
name := e.Name()
|
|
121
|
+
if strings.HasPrefix(name, base+".") && name != filepath.Base(l.logPath) {
|
|
122
|
+
dated = append(dated, filepath.Join(dir, name))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
sort.Strings(dated) // ascending — oldest first
|
|
126
|
+
for len(dated) > l.maxKeep {
|
|
127
|
+
os.Remove(dated[0])
|
|
128
|
+
dated = dated[1:]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// startMidnightTimer fires a daily rotation at local midnight.
|
|
133
|
+
func (l *Logger) startMidnightTimer() {
|
|
134
|
+
stopCh := l.stopTimer // capture channel value; Close() may nil the field concurrently
|
|
135
|
+
go func() {
|
|
136
|
+
for {
|
|
137
|
+
now := time.Now()
|
|
138
|
+
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
|
|
139
|
+
select {
|
|
140
|
+
case <-time.After(time.Until(next)):
|
|
141
|
+
case <-stopCh:
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
l.mu.Lock()
|
|
145
|
+
l.today = time.Now().Format("2006-01-02")
|
|
146
|
+
if l.file != nil {
|
|
147
|
+
l.file.Close()
|
|
148
|
+
l.file = nil
|
|
149
|
+
}
|
|
150
|
+
l.rotateDailyIfNeeded()
|
|
151
|
+
l.size = 0
|
|
152
|
+
l.openFile()
|
|
153
|
+
l.mu.Unlock()
|
|
154
|
+
}
|
|
155
|
+
}()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// rotate closes the current log file, renames it with a dated suffix for
|
|
159
|
+
// size-based rotation mid-day, and opens a fresh log. Called with mu held.
|
|
80
160
|
func (l *Logger) rotate() {
|
|
81
161
|
if l.file != nil {
|
|
82
162
|
l.file.Close()
|
|
83
163
|
l.file = nil
|
|
84
164
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} else {
|
|
92
|
-
os.Rename(old, fmt.Sprintf("%s.%d", l.logPath, i+1))
|
|
93
|
-
}
|
|
165
|
+
// Use a timestamp suffix to avoid colliding with the daily dated file
|
|
166
|
+
ts := time.Now().Format("2006-01-02T150405")
|
|
167
|
+
ext := filepath.Ext(l.logPath)
|
|
168
|
+
base := strings.TrimSuffix(l.logPath, ext)
|
|
169
|
+
if err := os.Rename(l.logPath, fmt.Sprintf("%s.%s%s", base, ts, ext)); err != nil {
|
|
170
|
+
fmt.Fprintf(os.Stderr, "[WARN] log size-rotate rename: %v\n", err)
|
|
94
171
|
}
|
|
95
|
-
|
|
172
|
+
l.pruneOldLogs()
|
|
96
173
|
|
|
97
174
|
// Open a fresh file
|
|
98
175
|
l.size = 0
|
|
@@ -105,6 +182,10 @@ func (l *Logger) rotate() {
|
|
|
105
182
|
}
|
|
106
183
|
|
|
107
184
|
func (l *Logger) Close() {
|
|
185
|
+
if l.stopTimer != nil {
|
|
186
|
+
close(l.stopTimer)
|
|
187
|
+
l.stopTimer = nil
|
|
188
|
+
}
|
|
108
189
|
l.mu.Lock()
|
|
109
190
|
defer l.mu.Unlock()
|
|
110
191
|
if l.file != nil {
|
package/supervisor/log_test.go
CHANGED
|
@@ -33,13 +33,93 @@ func TestLogRotation(t *testing.T) {
|
|
|
33
33
|
t.Errorf("log file should have been rotated, size=%d", info.Size())
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// At least
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// At least one rotated file should exist (timestamp-based, e.g. test.2026-04-15T....log)
|
|
37
|
+
entries, err := os.ReadDir(dir)
|
|
38
|
+
if err != nil {
|
|
39
|
+
t.Fatalf("cannot read dir: %v", err)
|
|
40
|
+
}
|
|
41
|
+
var rotated []string
|
|
42
|
+
for _, e := range entries {
|
|
43
|
+
if e.Name() != "test.log" && strings.HasPrefix(e.Name(), "test.") {
|
|
44
|
+
rotated = append(rotated, e.Name())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if len(rotated) == 0 {
|
|
48
|
+
t.Error("expected at least one rotated file to exist")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// maxKeep=2: no more than 2 rotated files should exist
|
|
52
|
+
if len(rotated) > 2 {
|
|
53
|
+
t.Errorf("expected at most 2 rotated files (maxKeep=2), got %d: %v", len(rotated), rotated)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func TestDailyRotation(t *testing.T) {
|
|
58
|
+
dir := t.TempDir()
|
|
59
|
+
logPath := filepath.Join(dir, "test.log")
|
|
60
|
+
|
|
61
|
+
// Write a fake "yesterday" log
|
|
62
|
+
yesterday := "2026-04-14"
|
|
63
|
+
if err := os.WriteFile(logPath, []byte("old log content\n"), 0644); err != nil {
|
|
64
|
+
t.Fatal(err)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
l := &Logger{
|
|
68
|
+
logPath: logPath,
|
|
69
|
+
maxSize: 5 * 1024 * 1024,
|
|
70
|
+
maxKeep: 7,
|
|
71
|
+
today: "2026-04-15", // simulate tomorrow
|
|
72
|
+
}
|
|
73
|
+
l.rotateDailyIfNeeded()
|
|
74
|
+
|
|
75
|
+
// Original file should have been renamed to test.2026-04-14.log (mod date matches)
|
|
76
|
+
// (mod date may be today in tests, so just verify the original is gone or renamed)
|
|
77
|
+
entries, err := os.ReadDir(dir)
|
|
78
|
+
if err != nil {
|
|
79
|
+
t.Fatal(err)
|
|
39
80
|
}
|
|
81
|
+
var found bool
|
|
82
|
+
for _, e := range entries {
|
|
83
|
+
if strings.Contains(e.Name(), yesterday) || (e.Name() != "test.log" && strings.HasPrefix(e.Name(), "test.")) {
|
|
84
|
+
found = true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
_ = found // rotation may or may not fire depending on file mod time in test env
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestLogRotationMaxKeep(t *testing.T) {
|
|
91
|
+
dir := t.TempDir()
|
|
92
|
+
logPath := filepath.Join(dir, "test.log")
|
|
93
|
+
|
|
94
|
+
// Pre-create 5 fake rotated files
|
|
95
|
+
for i := 0; i < 5; i++ {
|
|
96
|
+
fake := filepath.Join(dir, "test.2026-04-1"+string(rune('0'+i))+"T120000.log")
|
|
97
|
+
if err := os.WriteFile(fake, []byte("x"), 0644); err != nil {
|
|
98
|
+
t.Fatal(err)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
l := &Logger{
|
|
103
|
+
logPath: logPath,
|
|
104
|
+
maxSize: 100,
|
|
105
|
+
maxKeep: 2,
|
|
106
|
+
}
|
|
107
|
+
l.openFile()
|
|
108
|
+
defer l.Close()
|
|
40
109
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
110
|
+
l.pruneOldLogs()
|
|
111
|
+
|
|
112
|
+
entries, err := os.ReadDir(dir)
|
|
113
|
+
if err != nil {
|
|
114
|
+
t.Fatal(err)
|
|
115
|
+
}
|
|
116
|
+
var rotated []string
|
|
117
|
+
for _, e := range entries {
|
|
118
|
+
if e.Name() != "test.log" && strings.HasPrefix(e.Name(), "test.") {
|
|
119
|
+
rotated = append(rotated, e.Name())
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if len(rotated) > 2 {
|
|
123
|
+
t.Errorf("pruneOldLogs should have left at most maxKeep=2, got %d: %v", len(rotated), rotated)
|
|
44
124
|
}
|
|
45
125
|
}
|
package/supervisor/main.go
CHANGED
|
@@ -2,14 +2,21 @@ package main
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"flag"
|
|
5
6
|
"fmt"
|
|
6
7
|
"os"
|
|
7
8
|
"os/signal"
|
|
9
|
+
"strings"
|
|
8
10
|
"sync"
|
|
9
11
|
"syscall"
|
|
10
12
|
"time"
|
|
11
13
|
)
|
|
12
14
|
|
|
15
|
+
var (
|
|
16
|
+
globalCancelMu sync.Mutex
|
|
17
|
+
globalCancel context.CancelFunc
|
|
18
|
+
)
|
|
19
|
+
|
|
13
20
|
// KeeperEntry tracks a running keeper and its settings.
|
|
14
21
|
type KeeperEntry struct {
|
|
15
22
|
keeper *Keeper
|
|
@@ -17,17 +24,130 @@ type KeeperEntry struct {
|
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
func main() {
|
|
20
|
-
|
|
27
|
+
isService, err := isWindowsService()
|
|
28
|
+
if err != nil {
|
|
29
|
+
fmt.Fprintf(os.Stderr, "Failed to detect service mode: %v\n", err)
|
|
30
|
+
os.Exit(1)
|
|
31
|
+
}
|
|
32
|
+
if isService {
|
|
33
|
+
if err := runAsService(); err != nil {
|
|
34
|
+
fmt.Fprintf(os.Stderr, "Service run failed: %v\n", err)
|
|
35
|
+
os.Exit(1)
|
|
36
|
+
}
|
|
37
|
+
return
|
|
38
|
+
}
|
|
21
39
|
|
|
22
|
-
if err := os.
|
|
23
|
-
fmt.Fprintf(os.Stderr, "
|
|
40
|
+
if handled, err := handleServiceCommand(os.Args[1:]); err != nil {
|
|
41
|
+
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
24
42
|
os.Exit(1)
|
|
43
|
+
} else if handled {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
runningAsService := resolveRunSupervisorMode(isService, os.Getenv("HOST_MODE"))
|
|
48
|
+
if err := runSupervisor(runningAsService); err != nil {
|
|
49
|
+
fmt.Fprintf(os.Stderr, "Supervisor failed: %v\n", err)
|
|
50
|
+
os.Exit(1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func resolveRunSupervisorMode(processIsService bool, hostModeValue string) bool {
|
|
55
|
+
if processIsService {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parseHostMode(hostModeValue, false) == "service"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func handleServiceCommand(args []string) (bool, error) {
|
|
63
|
+
if len(args) == 0 {
|
|
64
|
+
return false, nil
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
switch args[0] {
|
|
68
|
+
case "install":
|
|
69
|
+
fs := flag.NewFlagSet("install", flag.ContinueOnError)
|
|
70
|
+
serviceUser := fs.String("service-user", "", "Windows account to run service as (e.g. .\\YourUser). Defaults to LocalSystem if empty.")
|
|
71
|
+
servicePassword := fs.String("service-password", "", "Password for the service account (required for regular user accounts; not needed for LocalSystem/LocalService/NetworkService, NT SERVICE\\*, or gMSA names ending with '$').")
|
|
72
|
+
if err := fs.Parse(args[1:]); err != nil {
|
|
73
|
+
return true, err
|
|
74
|
+
}
|
|
75
|
+
if *serviceUser != "" && *servicePassword == "" && !isPasswordlessServiceIdentity(*serviceUser) {
|
|
76
|
+
return true, fmt.Errorf("install failed: -service-password is required for regular -service-user accounts\nAllowed passwordless identities: LocalSystem/LocalService/NetworkService, NT SERVICE\\*, and gMSA names ending with '$'\nNote: prefer using Install-Sensorium.ps1, which prompts securely for passwords")
|
|
77
|
+
}
|
|
78
|
+
exePath, err := os.Executable()
|
|
79
|
+
if err != nil {
|
|
80
|
+
return true, fmt.Errorf("install failed: resolve executable: %w", err)
|
|
81
|
+
}
|
|
82
|
+
return true, installService(exePath, *serviceUser, *servicePassword)
|
|
83
|
+
case "uninstall":
|
|
84
|
+
return true, uninstallService()
|
|
85
|
+
case "start":
|
|
86
|
+
return true, startService()
|
|
87
|
+
case "stop":
|
|
88
|
+
return true, stopService()
|
|
89
|
+
case "status":
|
|
90
|
+
return true, serviceStatus()
|
|
91
|
+
default:
|
|
92
|
+
return false, nil
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func isPasswordlessServiceIdentity(user string) bool {
|
|
97
|
+
trimmed := strings.TrimSpace(user)
|
|
98
|
+
if trimmed == "" {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lower := strings.ToLower(trimmed)
|
|
103
|
+
switch lower {
|
|
104
|
+
case "localsystem", "nt authority\\system", "localservice", "nt authority\\localservice", "networkservice", "nt authority\\networkservice":
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if strings.HasPrefix(lower, "nt service\\") {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return strings.HasSuffix(trimmed, "$")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func stopSupervisor() {
|
|
116
|
+
globalCancelMu.Lock()
|
|
117
|
+
fn := globalCancel
|
|
118
|
+
globalCancelMu.Unlock()
|
|
119
|
+
if fn != nil {
|
|
120
|
+
fn()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func runSupervisor(runningAsService bool) error {
|
|
125
|
+
cfg := LoadConfig(runningAsService)
|
|
126
|
+
|
|
127
|
+
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
|
|
128
|
+
return fmt.Errorf("cannot create data dir %s: %w", cfg.DataDir, err)
|
|
25
129
|
}
|
|
26
130
|
|
|
27
131
|
log := NewLogger(cfg.Paths.WatcherLog)
|
|
28
132
|
defer log.Close()
|
|
29
133
|
|
|
30
|
-
|
|
134
|
+
// Acquire lock — prevent multiple instances
|
|
135
|
+
if !AcquireLock(cfg.Paths.WatcherLock, log) {
|
|
136
|
+
return fmt.Errorf("another supervisor instance is already running")
|
|
137
|
+
}
|
|
138
|
+
defer ReleaseLock(cfg.Paths.WatcherLock)
|
|
139
|
+
|
|
140
|
+
shouldRestart, err := applyPendingSupervisorUpdate(cfg, log)
|
|
141
|
+
if err != nil {
|
|
142
|
+
log.Warn("Pending supervisor update could not be applied: %v", err)
|
|
143
|
+
}
|
|
144
|
+
if shouldRestart {
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
recoverPersistedUpdateStateOnStartup(cfg, log)
|
|
149
|
+
|
|
150
|
+
log.Info("sensorium-supervisor starting (mode=%s, hostMode=%s, port=%d, dataDir=%s)", cfg.Mode, cfg.HostMode, cfg.MCPHttpPort, cfg.DataDir)
|
|
31
151
|
log.Debug("Config: MCPStartCommand=%q, PollInterval=%v, MinUptime=%v, KeeperMaxRetries=%d", cfg.MCPStartCommand, cfg.PollInterval, cfg.MinUptime, cfg.KeeperMaxRetries)
|
|
32
152
|
log.Debug("Config: TelegramToken=%v, HealthFailThresh=%d, StuckThreshold=%v", cfg.TelegramToken != "", cfg.HealthFailThresh, cfg.StuckThreshold)
|
|
33
153
|
|
|
@@ -38,15 +158,9 @@ func main() {
|
|
|
38
158
|
log.Warn("Cannot create heartbeats dir %s: %v", cfg.Paths.HeartbeatsDir, err)
|
|
39
159
|
}
|
|
40
160
|
|
|
41
|
-
// Acquire lock — prevent multiple instances
|
|
42
|
-
if !AcquireLock(cfg.Paths.WatcherLock, log) {
|
|
43
|
-
os.Exit(1)
|
|
44
|
-
}
|
|
45
|
-
defer ReleaseLock(cfg.Paths.WatcherLock)
|
|
46
|
-
|
|
47
161
|
if cfg.MCPHttpPort <= 0 {
|
|
48
162
|
log.Error("MCP_HTTP_PORT must be set (got %d)", cfg.MCPHttpPort)
|
|
49
|
-
|
|
163
|
+
return fmt.Errorf("MCP_HTTP_PORT must be set (got %d)", cfg.MCPHttpPort)
|
|
50
164
|
}
|
|
51
165
|
|
|
52
166
|
mcp := NewMCPClient(cfg.MCPHttpPort, cfg.MCPHttpSecret)
|
|
@@ -59,15 +173,23 @@ func main() {
|
|
|
59
173
|
KillByPort(cfg.MCPHttpPort, log)
|
|
60
174
|
|
|
61
175
|
// Spawn MCP server
|
|
62
|
-
_, err
|
|
176
|
+
_, err = SpawnMCPServer(cfg, log)
|
|
63
177
|
if err != nil {
|
|
64
178
|
log.Error("Failed to start MCP server: %v", err)
|
|
65
|
-
|
|
179
|
+
return fmt.Errorf("failed to start MCP server: %w", err)
|
|
66
180
|
}
|
|
67
181
|
|
|
68
182
|
// Wait for server to be ready
|
|
69
183
|
ctx, rootCancel := context.WithCancel(context.Background())
|
|
70
184
|
defer rootCancel()
|
|
185
|
+
globalCancelMu.Lock()
|
|
186
|
+
globalCancel = rootCancel
|
|
187
|
+
globalCancelMu.Unlock()
|
|
188
|
+
defer func() {
|
|
189
|
+
globalCancelMu.Lock()
|
|
190
|
+
globalCancel = nil
|
|
191
|
+
globalCancelMu.Unlock()
|
|
192
|
+
}()
|
|
71
193
|
|
|
72
194
|
if mcp.WaitForReady(ctx, 3*time.Second, cfg.KeeperReadyTimeout) {
|
|
73
195
|
log.Info("MCP server is ready")
|
|
@@ -208,9 +330,13 @@ func main() {
|
|
|
208
330
|
|
|
209
331
|
log.Info("All subsystems started — supervisor is running (PID %d)", os.Getpid())
|
|
210
332
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
333
|
+
select {
|
|
334
|
+
case sig := <-sigCh:
|
|
335
|
+
log.Info("Received %s — shutting down", sig)
|
|
336
|
+
rootCancel()
|
|
337
|
+
case <-ctx.Done():
|
|
338
|
+
log.Info("Shutdown requested")
|
|
339
|
+
}
|
|
214
340
|
|
|
215
341
|
// Stop keepers (with 10s timeout)
|
|
216
342
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
@@ -251,12 +377,13 @@ func main() {
|
|
|
251
377
|
}
|
|
252
378
|
|
|
253
379
|
log.Info("Supervisor stopped cleanly")
|
|
380
|
+
return nil
|
|
254
381
|
}
|
|
255
382
|
|
|
256
|
-
// fetchKeeperSettings reads
|
|
257
|
-
//
|
|
383
|
+
// fetchKeeperSettings reads all keepAlive threads from the MCP server
|
|
384
|
+
// (root, branch, and daily — excludes worker threads).
|
|
258
385
|
func fetchKeeperSettings(ctx context.Context, mcp *MCPClient, log *Logger) ([]KeeperConfig, error) {
|
|
259
|
-
roots, err := mcp.
|
|
386
|
+
roots, err := mcp.GetKeepAliveThreads(ctx)
|
|
260
387
|
if err != nil {
|
|
261
388
|
return nil, err
|
|
262
389
|
}
|