sensorium-mcp 3.0.4 → 3.0.6

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 (120) hide show
  1. package/Install-Sensorium.ps1 +102 -209
  2. package/dist/dashboard/routes/data.d.ts.map +1 -1
  3. package/dist/dashboard/routes/data.js +2 -1
  4. package/dist/dashboard/routes/data.js.map +1 -1
  5. package/dist/dashboard/routes/threads.js +1 -1
  6. package/dist/dashboard/routes/threads.js.map +1 -1
  7. package/dist/dashboard/routes.d.ts.map +1 -1
  8. package/dist/dashboard/routes.js +1 -3
  9. package/dist/dashboard/routes.js.map +1 -1
  10. package/dist/data/memory/migration-runner.d.ts +1 -1
  11. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  12. package/dist/data/memory/migration-runner.js +59 -3
  13. package/dist/data/memory/migration-runner.js.map +1 -1
  14. package/dist/data/memory/narrative.d.ts.map +1 -1
  15. package/dist/data/memory/narrative.js +43 -6
  16. package/dist/data/memory/narrative.js.map +1 -1
  17. package/dist/data/memory/reflection.d.ts +24 -0
  18. package/dist/data/memory/reflection.d.ts.map +1 -1
  19. package/dist/data/memory/reflection.js +65 -1
  20. package/dist/data/memory/reflection.js.map +1 -1
  21. package/dist/data/memory/schema-ddl.d.ts +1 -1
  22. package/dist/data/memory/schema-ddl.d.ts.map +1 -1
  23. package/dist/data/memory/schema-ddl.js +2 -1
  24. package/dist/data/memory/schema-ddl.js.map +1 -1
  25. package/dist/data/memory/thread-registry.js +1 -1
  26. package/dist/data/memory/thread-registry.js.map +1 -1
  27. package/dist/http-server.d.ts.map +1 -1
  28. package/dist/http-server.js +1 -9
  29. package/dist/http-server.js.map +1 -1
  30. package/dist/index.js +3 -6
  31. package/dist/index.js.map +1 -1
  32. package/dist/server/factory.js +1 -1
  33. package/dist/server/factory.js.map +1 -1
  34. package/dist/services/agent-spawn.service.d.ts +7 -1
  35. package/dist/services/agent-spawn.service.d.ts.map +1 -1
  36. package/dist/services/agent-spawn.service.js +69 -45
  37. package/dist/services/agent-spawn.service.js.map +1 -1
  38. package/dist/services/consolidation.service.d.ts.map +1 -1
  39. package/dist/services/consolidation.service.js +88 -35
  40. package/dist/services/consolidation.service.js.map +1 -1
  41. package/dist/services/keeper.service.d.ts +21 -0
  42. package/dist/services/keeper.service.d.ts.map +1 -0
  43. package/dist/services/keeper.service.js +195 -0
  44. package/dist/services/keeper.service.js.map +1 -0
  45. package/dist/services/maintenance-signal.d.ts +2 -0
  46. package/dist/services/maintenance-signal.d.ts.map +1 -1
  47. package/dist/services/maintenance-signal.js +7 -1
  48. package/dist/services/maintenance-signal.js.map +1 -1
  49. package/dist/services/memory-briefing.service.d.ts.map +1 -1
  50. package/dist/services/memory-briefing.service.js +17 -1
  51. package/dist/services/memory-briefing.service.js.map +1 -1
  52. package/dist/services/process.service.d.ts +19 -2
  53. package/dist/services/process.service.d.ts.map +1 -1
  54. package/dist/services/process.service.js +104 -10
  55. package/dist/services/process.service.js.map +1 -1
  56. package/dist/services/thread-lifecycle.service.d.ts +5 -0
  57. package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
  58. package/dist/services/thread-lifecycle.service.js +33 -8
  59. package/dist/services/thread-lifecycle.service.js.map +1 -1
  60. package/dist/services/worker-cleanup.service.d.ts +14 -1
  61. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  62. package/dist/services/worker-cleanup.service.js +36 -38
  63. package/dist/services/worker-cleanup.service.js.map +1 -1
  64. package/dist/sessions.d.ts +0 -5
  65. package/dist/sessions.d.ts.map +1 -1
  66. package/dist/sessions.js +0 -7
  67. package/dist/sessions.js.map +1 -1
  68. package/dist/stdio-server.d.ts.map +1 -1
  69. package/dist/stdio-server.js +1 -7
  70. package/dist/stdio-server.js.map +1 -1
  71. package/dist/tools/delegate-tool.d.ts.map +1 -1
  72. package/dist/tools/delegate-tool.js +2 -2
  73. package/dist/tools/delegate-tool.js.map +1 -1
  74. package/dist/tools/session-tools.js +1 -1
  75. package/dist/tools/session-tools.js.map +1 -1
  76. package/dist/tools/start-session-tool.d.ts.map +1 -1
  77. package/dist/tools/start-session-tool.js +8 -9
  78. package/dist/tools/start-session-tool.js.map +1 -1
  79. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  80. package/dist/tools/wait/message-processing.js +28 -0
  81. package/dist/tools/wait/message-processing.js.map +1 -1
  82. package/dist/tools/wait/poll-loop.js +1 -1
  83. package/dist/tools/wait/poll-loop.js.map +1 -1
  84. package/package.json +1 -1
  85. package/dist/tools/thread-lifecycle.d.ts +0 -6
  86. package/dist/tools/thread-lifecycle.d.ts.map +0 -1
  87. package/dist/tools/thread-lifecycle.js +0 -6
  88. package/dist/tools/thread-lifecycle.js.map +0 -1
  89. package/supervisor/config.go +0 -253
  90. package/supervisor/config_test.go +0 -78
  91. package/supervisor/go.mod +0 -15
  92. package/supervisor/go.sum +0 -20
  93. package/supervisor/health.go +0 -433
  94. package/supervisor/health_test.go +0 -93
  95. package/supervisor/keeper.go +0 -309
  96. package/supervisor/keeper_test.go +0 -27
  97. package/supervisor/lock.go +0 -57
  98. package/supervisor/lock_test.go +0 -54
  99. package/supervisor/log.go +0 -195
  100. package/supervisor/log_test.go +0 -125
  101. package/supervisor/main.go +0 -475
  102. package/supervisor/main_test.go +0 -130
  103. package/supervisor/notify.go +0 -53
  104. package/supervisor/process.go +0 -294
  105. package/supervisor/process_test.go +0 -108
  106. package/supervisor/process_unix.go +0 -14
  107. package/supervisor/process_windows.go +0 -15
  108. package/supervisor/secrets.go +0 -95
  109. package/supervisor/secrets_securevault_test.go +0 -98
  110. package/supervisor/secrets_test.go +0 -119
  111. package/supervisor/self_update.go +0 -282
  112. package/supervisor/self_update_test.go +0 -177
  113. package/supervisor/service_restart_stub.go +0 -9
  114. package/supervisor/service_restart_windows.go +0 -63
  115. package/supervisor/service_stub.go +0 -15
  116. package/supervisor/service_windows.go +0 -194
  117. package/supervisor/update_state.go +0 -264
  118. package/supervisor/update_state_test.go +0 -306
  119. package/supervisor/updater.go +0 -613
  120. package/supervisor/updater_test.go +0 -64
@@ -1,98 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "os"
6
- "path/filepath"
7
- "testing"
8
-
9
- sv "github.com/andriyshevchenko/SecureVault/securevault-go"
10
- "github.com/zalando/go-keyring"
11
- )
12
-
13
- // writeProfileFixture writes a minimal profiles.json to dir and returns the dir.
14
- func writeProfileFixture(t *testing.T, dir string, profiles []sv.Profile) {
15
- t.Helper()
16
- data, err := json.Marshal(profiles)
17
- if err != nil {
18
- t.Fatalf("marshal profiles: %v", err)
19
- }
20
- if err := os.WriteFile(filepath.Join(dir, "profiles.json"), data, 0600); err != nil {
21
- t.Fatalf("write profiles.json: %v", err)
22
- }
23
- }
24
-
25
- func TestResolveStringChain_EnvWins(t *testing.T) {
26
- t.Setenv("TELEGRAM_TOKEN", "env-value")
27
- dir := t.TempDir()
28
- writeProfileFixture(t, dir, []sv.Profile{
29
- {ID: "p1", Name: "TEST", Mappings: []sv.ProfileMapping{
30
- {EnvVar: "TELEGRAM_TOKEN", SecretID: "no-cred"},
31
- }},
32
- })
33
-
34
- got := resolveStringChain("TELEGRAM_TOKEN", "TEST", dir, "")
35
- if got != "env-value" {
36
- t.Errorf("resolveStringChain = %q, want env-value", got)
37
- }
38
- }
39
-
40
- func TestResolveStringChain_SecureVaultFallback_MissingCred_UsesKeyring(t *testing.T) {
41
- t.Setenv("TELEGRAM_TOKEN", "")
42
- dir := t.TempDir()
43
- writeProfileFixture(t, dir, []sv.Profile{
44
- {ID: "p1", Name: "TEST", Mappings: []sv.ProfileMapping{
45
- {EnvVar: "TELEGRAM_TOKEN", SecretID: "non-existent-cred"},
46
- }},
47
- })
48
-
49
- orig := keyringGet
50
- keyringGet = func(service, user string) (string, error) {
51
- if service != "sensorium-supervisor" {
52
- t.Fatalf("service = %q, want %q", service, "sensorium-supervisor")
53
- }
54
- if user != "TELEGRAM_TOKEN" {
55
- t.Fatalf("user = %q, want %q", user, "TELEGRAM_TOKEN")
56
- }
57
- return "from-keyring", nil
58
- }
59
- t.Cleanup(func() { keyringGet = orig })
60
-
61
- // Credential not in store → falls through to keyring.
62
- got := resolveStringChain("TELEGRAM_TOKEN", "TEST", dir, "sensorium-supervisor")
63
- if got != "from-keyring" {
64
- t.Errorf("resolveStringChain = %q, want from-keyring", got)
65
- }
66
- }
67
-
68
- func TestResolveStringChain_NoProfile_FallsToKeyring(t *testing.T) {
69
- t.Setenv("TELEGRAM_TOKEN", "")
70
-
71
- orig := keyringGet
72
- keyringGet = func(service, user string) (string, error) {
73
- return "", keyring.ErrNotFound
74
- }
75
- t.Cleanup(func() { keyringGet = orig })
76
-
77
- // No profiles file in dir → securevault returns empty → keyring path taken.
78
- got := resolveStringChain("TELEGRAM_TOKEN", "NONEXISTENT_PROFILE", t.TempDir(), "sensorium-supervisor")
79
- if got != "" {
80
- t.Errorf("resolveStringChain = %q, want empty", got)
81
- }
82
- }
83
-
84
- func TestResolveIntChain_EnvWins(t *testing.T) {
85
- t.Setenv("MCP_HTTP_PORT", "4567")
86
- got := resolveIntChain("MCP_HTTP_PORT", "TEST", t.TempDir(), "", 0)
87
- if got != 4567 {
88
- t.Errorf("resolveIntChain = %d, want 4567", got)
89
- }
90
- }
91
-
92
- func TestResolveIntChain_InvalidFallback(t *testing.T) {
93
- t.Setenv("MCP_HTTP_PORT", "not-a-number")
94
- got := resolveIntChain("MCP_HTTP_PORT", "TEST", t.TempDir(), "", 9999)
95
- if got != 9999 {
96
- t.Errorf("resolveIntChain invalid = %d, want fallback 9999", got)
97
- }
98
- }
@@ -1,119 +0,0 @@
1
- package main
2
-
3
- import (
4
- "errors"
5
- "testing"
6
-
7
- "github.com/zalando/go-keyring"
8
- )
9
-
10
- func TestResolveSecretWithKeyring_EnvWins(t *testing.T) {
11
- t.Setenv("MCP_HTTP_SECRET", "env-secret")
12
-
13
- orig := keyringGet
14
- keyringGet = func(service, user string) (string, error) {
15
- return "keyring-secret", nil
16
- }
17
- t.Cleanup(func() { keyringGet = orig })
18
-
19
- got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
20
- if got != "env-secret" {
21
- t.Fatalf("resolveSecretWithKeyring() = %q, want %q", got, "env-secret")
22
- }
23
- }
24
-
25
- func TestResolveSecretWithKeyring_FallbackToKeyring(t *testing.T) {
26
- t.Setenv("MCP_HTTP_SECRET", "")
27
-
28
- orig := keyringGet
29
- keyringGet = func(service, user string) (string, error) {
30
- if service != "sensorium-supervisor" {
31
- t.Fatalf("service = %q, want %q", service, "sensorium-supervisor")
32
- }
33
- if user != "MCP_HTTP_SECRET" {
34
- t.Fatalf("user = %q, want %q", user, "MCP_HTTP_SECRET")
35
- }
36
- return "keyring-secret", nil
37
- }
38
- t.Cleanup(func() { keyringGet = orig })
39
-
40
- got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
41
- if got != "keyring-secret" {
42
- t.Fatalf("resolveSecretWithKeyring() = %q, want %q", got, "keyring-secret")
43
- }
44
- }
45
-
46
- func TestResolveSecretWithKeyring_NotFound(t *testing.T) {
47
- t.Setenv("MCP_HTTP_SECRET", "")
48
-
49
- orig := keyringGet
50
- keyringGet = func(service, user string) (string, error) {
51
- return "", keyring.ErrNotFound
52
- }
53
- t.Cleanup(func() { keyringGet = orig })
54
-
55
- got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
56
- if got != "" {
57
- t.Fatalf("resolveSecretWithKeyring() = %q, want empty", got)
58
- }
59
- }
60
-
61
- func TestResolveSecretWithKeyring_OtherError(t *testing.T) {
62
- t.Setenv("MCP_HTTP_SECRET", "")
63
-
64
- orig := keyringGet
65
- keyringGet = func(service, user string) (string, error) {
66
- return "", errors.New("keyring backend unavailable")
67
- }
68
- t.Cleanup(func() { keyringGet = orig })
69
-
70
- got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
71
- if got != "" {
72
- t.Fatalf("resolveSecretWithKeyring() = %q, want empty", got)
73
- }
74
- }
75
-
76
- func TestResolveIntWithKeyring_EnvWins(t *testing.T) {
77
- t.Setenv("MCP_HTTP_PORT", "3847")
78
-
79
- orig := keyringGet
80
- keyringGet = func(service, user string) (string, error) {
81
- return "9999", nil
82
- }
83
- t.Cleanup(func() { keyringGet = orig })
84
-
85
- got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 0)
86
- if got != 3847 {
87
- t.Fatalf("resolveIntWithKeyring() = %d, want %d", got, 3847)
88
- }
89
- }
90
-
91
- func TestResolveIntWithKeyring_FallbackToKeyring(t *testing.T) {
92
- t.Setenv("MCP_HTTP_PORT", "")
93
-
94
- orig := keyringGet
95
- keyringGet = func(service, user string) (string, error) {
96
- return "5001", nil
97
- }
98
- t.Cleanup(func() { keyringGet = orig })
99
-
100
- got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 0)
101
- if got != 5001 {
102
- t.Fatalf("resolveIntWithKeyring() = %d, want %d", got, 5001)
103
- }
104
- }
105
-
106
- func TestResolveIntWithKeyring_InvalidFallback(t *testing.T) {
107
- t.Setenv("MCP_HTTP_PORT", "")
108
-
109
- orig := keyringGet
110
- keyringGet = func(service, user string) (string, error) {
111
- return "not-a-number", nil
112
- }
113
- t.Cleanup(func() { keyringGet = orig })
114
-
115
- got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 3847)
116
- if got != 3847 {
117
- t.Fatalf("resolveIntWithKeyring() = %d, want fallback %d", got, 3847)
118
- }
119
- }
@@ -1,282 +0,0 @@
1
- package main
2
-
3
- import (
4
- "errors"
5
- "fmt"
6
- "os"
7
- "os/exec"
8
- "path/filepath"
9
- "runtime"
10
- "strings"
11
- )
12
-
13
- const supervisorRollbackAttemptedMarker = "rollback_attempted=true"
14
- const supervisorWindowsServiceName = "SensoriumSupervisor"
15
-
16
- // applyPendingSupervisorUpdate applies a downloaded supervisor binary before the
17
- // rest of the process starts. On Windows, a detached helper performs the swap
18
- // after this bootstrap process exits because a running .exe cannot overwrite
19
- // itself in place.
20
- func applyPendingSupervisorUpdate(cfg Config, log *Logger) (bool, error) {
21
- recordPendingSupervisorApplyFailureIfPresent(cfg, log)
22
-
23
- if _, err := os.Stat(cfg.Paths.PendingBinary); err != nil {
24
- if errors.Is(err, os.ErrNotExist) {
25
- if _, versionErr := os.Stat(cfg.Paths.PendingVersion); versionErr == nil {
26
- log.Warn("Removing stale pending supervisor version file %s", cfg.Paths.PendingVersion)
27
- _ = os.Remove(cfg.Paths.PendingVersion)
28
- }
29
- return false, nil
30
- }
31
- return false, fmt.Errorf("stat pending supervisor binary: %w", err)
32
- }
33
-
34
- exePath, err := os.Executable()
35
- if err != nil {
36
- cleanupPendingSupervisorUpdate(cfg, log)
37
- return false, fmt.Errorf("resolve current executable: %w", err)
38
- }
39
-
40
- if runtime.GOOS == "windows" {
41
- isService, serviceErr := isWindowsService()
42
- if serviceErr != nil {
43
- markSupervisorApplyFailure(cfg, log, fmt.Sprintf("detect service mode for pending supervisor apply: %v", serviceErr))
44
- cleanupPendingSupervisorUpdate(cfg, log)
45
- return false, fmt.Errorf("detect service mode for pending supervisor apply: %w", serviceErr)
46
- }
47
-
48
- if err := launchWindowsApplyHelper(cfg, exePath, isService); err != nil {
49
- markSupervisorApplyFailure(cfg, log, fmt.Sprintf("schedule pending supervisor apply: %v", err))
50
- cleanupPendingSupervisorUpdate(cfg, log)
51
- return false, fmt.Errorf("schedule pending supervisor apply: %w", err)
52
- }
53
- log.Info("Pending supervisor update detected; restarting to apply")
54
- return true, nil
55
- }
56
-
57
- if err := os.Rename(cfg.Paths.PendingBinary, exePath); err != nil {
58
- cleanupPendingSupervisorUpdate(cfg, log)
59
- return false, fmt.Errorf("apply pending supervisor binary: %w", err)
60
- }
61
-
62
- if err := finalizePendingSupervisorVersion(cfg); err != nil {
63
- log.Warn("Applied supervisor update but failed to persist version: %v", err)
64
- } else {
65
- log.Info("Applied supervisor update")
66
- }
67
-
68
- return false, nil
69
- }
70
-
71
- func finalizePendingSupervisorVersion(cfg Config) error {
72
- data, err := os.ReadFile(cfg.Paths.PendingVersion)
73
- if err != nil {
74
- if errors.Is(err, os.ErrNotExist) {
75
- return nil
76
- }
77
- return fmt.Errorf("read pending supervisor version: %w", err)
78
- }
79
-
80
- if err := atomicWrite(cfg.Paths.SupervisorVersion, data); err != nil {
81
- return fmt.Errorf("write supervisor version: %w", err)
82
- }
83
- if err := os.Remove(cfg.Paths.PendingVersion); err != nil && !errors.Is(err, os.ErrNotExist) {
84
- return fmt.Errorf("remove pending supervisor version: %w", err)
85
- }
86
- return nil
87
- }
88
-
89
- func cleanupPendingSupervisorUpdate(cfg Config, log *Logger) {
90
- for _, path := range []string{cfg.Paths.PendingBinary, cfg.Paths.PendingVersion} {
91
- if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
92
- log.Warn("Failed to remove stale supervisor update artifact %s: %v", path, err)
93
- }
94
- }
95
- }
96
-
97
- func launchWindowsApplyHelper(cfg Config, exePath string, restartViaService bool) error {
98
- if err := os.MkdirAll(filepath.Dir(cfg.Paths.SupervisorVersion), 0755); err != nil {
99
- return fmt.Errorf("create supervisor version directory: %w", err)
100
- }
101
-
102
- scriptFile, err := os.CreateTemp("", "sensorium-supervisor-apply-*.cmd")
103
- if err != nil {
104
- return fmt.Errorf("create apply helper script: %w", err)
105
- }
106
-
107
- script := buildWindowsApplyHelperScript(cfg, exePath, restartViaService)
108
-
109
- if _, err := scriptFile.WriteString(script); err != nil {
110
- scriptFile.Close()
111
- _ = os.Remove(scriptFile.Name())
112
- return fmt.Errorf("write apply helper script: %w", err)
113
- }
114
- if err := scriptFile.Close(); err != nil {
115
- _ = os.Remove(scriptFile.Name())
116
- return fmt.Errorf("close apply helper script: %w", err)
117
- }
118
-
119
- cmd := exec.Command("cmd", "/c", scriptFile.Name())
120
- cmd.Env = os.Environ()
121
- cmd.Stdin = nil
122
- cmd.Stdout = nil
123
- cmd.Stderr = nil
124
- setSysProcAttr(cmd)
125
-
126
- if err := cmd.Start(); err != nil {
127
- _ = os.Remove(scriptFile.Name())
128
- return fmt.Errorf("start apply helper: %w", err)
129
- }
130
-
131
- _ = cmd.Process.Release()
132
- return nil
133
- }
134
-
135
- func buildWindowsApplyHelperScript(cfg Config, exePath string, restartViaService bool) string {
136
- failReason := fmt.Sprintf("helper failed to swap pending supervisor binary after retries (pending=%s current=%s)", cfg.Paths.PendingBinary, exePath)
137
- escapedFailReason := batchEscapeForSetValue(failReason)
138
-
139
- failLines := []string{
140
- fmt.Sprintf(`set "FAIL_REASON=%s"`, escapedFailReason),
141
- fmt.Sprintf(`<nul set /p "=%%FAIL_REASON%%" > %s`, batchQuote(supervisorApplyFailureMarkerPath(cfg))),
142
- fmt.Sprintf(`if exist %s del /F /Q %s`, batchQuote(cfg.Paths.PendingBinary), batchQuote(cfg.Paths.PendingBinary)),
143
- fmt.Sprintf(`if exist %s del /F /Q %s`, batchQuote(cfg.Paths.PendingVersion), batchQuote(cfg.Paths.PendingVersion)),
144
- }
145
-
146
- if restartViaService {
147
- failLines = append(failLines,
148
- fmt.Sprintf(`sc start %s >NUL 2>&1`, batchQuote(supervisorWindowsServiceName)),
149
- "if errorlevel 1 (",
150
- " timeout /T 2 /NOBREAK >NUL",
151
- fmt.Sprintf(` sc start %s >NUL 2>&1`, batchQuote(supervisorWindowsServiceName)),
152
- ")",
153
- )
154
- } else {
155
- failLines = append(failLines, fmt.Sprintf(`start "" %s`, batchQuote(exePath)))
156
- }
157
-
158
- failLines = append(failLines, "exit /b 1")
159
-
160
- scriptLines := []string{
161
- "@echo off",
162
- "setlocal",
163
- ":wait",
164
- fmt.Sprintf(`tasklist /FI "PID eq %d" 2>NUL | find "%d" >NUL`, os.Getpid(), os.Getpid()),
165
- "if not errorlevel 1 (",
166
- " timeout /T 1 /NOBREAK >NUL",
167
- " goto wait",
168
- ")",
169
- "set attempts=0",
170
- ":move",
171
- fmt.Sprintf(`move /Y %s %s >NUL`, batchQuote(cfg.Paths.PendingBinary), batchQuote(exePath)),
172
- "if not errorlevel 1 goto applied",
173
- "set /a attempts+=1",
174
- "if %attempts% GEQ 5 goto fail",
175
- "timeout /T 1 /NOBREAK >NUL",
176
- "goto move",
177
- ":applied",
178
- fmt.Sprintf(`if exist %s move /Y %s %s >NUL`, batchQuote(cfg.Paths.PendingVersion), batchQuote(cfg.Paths.PendingVersion), batchQuote(cfg.Paths.SupervisorVersion)),
179
- fmt.Sprintf(`if exist %s del /F /Q %s`, batchQuote(supervisorApplyFailureMarkerPath(cfg)), batchQuote(supervisorApplyFailureMarkerPath(cfg))),
180
- }
181
-
182
- if !restartViaService {
183
- scriptLines = append(scriptLines, fmt.Sprintf(`start "" %s`, batchQuote(exePath)))
184
- }
185
-
186
- scriptLines = append(scriptLines,
187
- "exit /b 0",
188
- ":fail",
189
- )
190
-
191
- scriptLines = append(scriptLines, failLines...)
192
- scriptLines = append(scriptLines, "")
193
-
194
- return strings.Join(scriptLines, "\r\n")
195
- }
196
-
197
- func supervisorApplyFailureMarkerPath(cfg Config) string {
198
- return cfg.Paths.PendingBinary + ".failed"
199
- }
200
-
201
- func recordPendingSupervisorApplyFailureIfPresent(cfg Config, log *Logger) {
202
- markerPath := supervisorApplyFailureMarkerPath(cfg)
203
- data, err := os.ReadFile(markerPath)
204
- if err != nil {
205
- if !errors.Is(err, os.ErrNotExist) {
206
- log.Warn("Failed to read supervisor apply failure marker %s: %v", markerPath, err)
207
- }
208
- return
209
- }
210
-
211
- reason := strings.TrimSpace(string(data))
212
- if reason == "" {
213
- reason = "pending supervisor apply helper reported failure"
214
- }
215
-
216
- store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
217
- state, stateErr := store.Load()
218
- if stateErr != nil {
219
- log.Warn("Failed to load update state while reconciling supervisor apply failure marker: %v", stateErr)
220
- }
221
- targetVersion := strings.TrimSpace(state.TargetVersion)
222
- if targetVersion == "" {
223
- targetVersion = strings.TrimSpace(readTrimmedFile(cfg.Paths.PendingVersion))
224
- }
225
- if targetVersion != "" {
226
- currentVersion := strings.TrimSpace(readTrimmedFile(cfg.Paths.SupervisorVersion))
227
- if currentVersion == targetVersion {
228
- log.Warn("Ignoring stale supervisor apply failure marker because supervisor version already matches target %s", targetVersion)
229
- store.Transition(updateScopeSupervisor, updatePhaseIdle, targetVersion, state.PreviousVersion, "")
230
- if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
231
- log.Warn("Failed to remove stale supervisor apply failure marker %s: %v", markerPath, err)
232
- }
233
- return
234
- }
235
- }
236
-
237
- markSupervisorApplyFailure(cfg, log, reason)
238
-
239
- if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
240
- log.Warn("Failed to remove supervisor apply failure marker %s: %v", markerPath, err)
241
- }
242
- }
243
-
244
- func markSupervisorApplyFailure(cfg Config, log *Logger, reason string) {
245
- store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
246
- state, err := store.Load()
247
- targetVersion := ""
248
- previousVersion := ""
249
- if err != nil {
250
- log.Warn("Failed to load update state while marking supervisor apply failure: %v", err)
251
- } else {
252
- targetVersion = state.TargetVersion
253
- previousVersion = state.PreviousVersion
254
- }
255
-
256
- if targetVersion == "" {
257
- targetVersion = strings.TrimSpace(readTrimmedFile(cfg.Paths.PendingVersion))
258
- }
259
- if previousVersion == "" {
260
- previousVersion = strings.TrimSpace(readTrimmedFile(cfg.Paths.SupervisorVersion))
261
- }
262
-
263
- lastError := reason
264
- if !strings.Contains(lastError, supervisorRollbackAttemptedMarker) {
265
- lastError = strings.TrimSpace(lastError + "; " + supervisorRollbackAttemptedMarker)
266
- }
267
-
268
- store.Transition(updateScopeSupervisor, updatePhaseRollback, targetVersion, previousVersion, lastError)
269
- store.Transition(updateScopeSupervisor, updatePhaseFailed, targetVersion, previousVersion, lastError)
270
- }
271
-
272
- func batchQuote(path string) string {
273
- return `"` + strings.ReplaceAll(path, `"`, `""`) + `"`
274
- }
275
-
276
- func batchEscapeForSetValue(value string) string {
277
- value = strings.ReplaceAll(value, "\r", " ")
278
- value = strings.ReplaceAll(value, "\n", " ")
279
- value = strings.ReplaceAll(value, `%`, `%%`)
280
- value = strings.ReplaceAll(value, `"`, `'`)
281
- return value
282
- }
@@ -1,177 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "path/filepath"
6
- "strings"
7
- "testing"
8
- )
9
-
10
- func TestApplyPendingSupervisorUpdate_ConvergesToFailedFromRollbackMarker(t *testing.T) {
11
- dir := t.TempDir()
12
- log := NewLogger(filepath.Join(dir, "test.log"))
13
- defer log.Close()
14
-
15
- cfg := Config{
16
- Paths: Paths{
17
- PendingBinary: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe"),
18
- PendingVersion: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe.version"),
19
- SupervisorVersion: filepath.Join(dir, "supervisor-version.txt"),
20
- UpdateState: filepath.Join(dir, "update-state.json"),
21
- },
22
- }
23
-
24
- if err := os.MkdirAll(filepath.Dir(cfg.Paths.PendingBinary), 0755); err != nil {
25
- t.Fatalf("create pending dir: %v", err)
26
- }
27
-
28
- store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
29
- store.Transition(updateScopeSupervisor, updatePhaseRestarting, "2.0.0", "1.0.0", "")
30
-
31
- reason := "helper failed to swap pending supervisor binary after retries"
32
- if err := os.WriteFile(supervisorApplyFailureMarkerPath(cfg), []byte(reason), 0644); err != nil {
33
- t.Fatalf("write apply failure marker: %v", err)
34
- }
35
-
36
- shouldRestart, err := applyPendingSupervisorUpdate(cfg, log)
37
- if err != nil {
38
- t.Fatalf("applyPendingSupervisorUpdate() error = %v", err)
39
- }
40
- if shouldRestart {
41
- t.Fatal("applyPendingSupervisorUpdate() shouldRestart = true, want false")
42
- }
43
-
44
- state, err := store.Load()
45
- if err != nil {
46
- t.Fatalf("load state: %v", err)
47
- }
48
- if state.Scope != updateScopeSupervisor {
49
- t.Fatalf("scope = %q, want %q", state.Scope, updateScopeSupervisor)
50
- }
51
- if state.Phase != updatePhaseFailed {
52
- t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseFailed)
53
- }
54
- if !strings.Contains(state.LastError, reason) {
55
- t.Fatalf("last error = %q, want reason %q", state.LastError, reason)
56
- }
57
- if !strings.Contains(state.LastError, supervisorRollbackAttemptedMarker) {
58
- t.Fatalf("last error = %q, missing rollback marker %q", state.LastError, supervisorRollbackAttemptedMarker)
59
- }
60
-
61
- if _, statErr := os.Stat(supervisorApplyFailureMarkerPath(cfg)); !os.IsNotExist(statErr) {
62
- t.Fatalf("expected apply failure marker to be removed, got: %v", statErr)
63
- }
64
- }
65
-
66
- func TestBuildWindowsApplyHelperScript_TaskModeFailPathRestartsCurrentBinary(t *testing.T) {
67
- cfg := Config{
68
- Paths: Paths{
69
- PendingBinary: `C:\data\bin\sensorium-supervisor.new.exe`,
70
- PendingVersion: `C:\data\bin\sensorium-supervisor.new.exe.version`,
71
- SupervisorVersion: `C:\data\supervisor-version.txt`,
72
- },
73
- }
74
- exePath := `C:\data\sensorium-supervisor.exe`
75
-
76
- script := buildWindowsApplyHelperScript(cfg, exePath, false)
77
-
78
- failStart := `:fail` + "\r\n" + `set "FAIL_REASON=helper failed to swap pending supervisor binary after retries (pending=C:\data\bin\sensorium-supervisor.new.exe current=C:\data\sensorium-supervisor.exe)"` + "\r\n" + `<nul set /p "=%FAIL_REASON%" > "C:\data\bin\sensorium-supervisor.new.exe.failed"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe.version" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe.version"` + "\r\n" + `start "" "C:\data\sensorium-supervisor.exe"`
79
- if !strings.Contains(script, failStart) {
80
- t.Fatalf("task-mode fail fallback restart block missing\nscript:\n%s", script)
81
- }
82
-
83
- if !strings.Contains(script, `:applied`+"\r\n"+`if exist "C:\data\bin\sensorium-supervisor.new.exe.version" move /Y "C:\data\bin\sensorium-supervisor.new.exe.version" "C:\data\supervisor-version.txt" >NUL`+"\r\n"+`if exist "C:\data\bin\sensorium-supervisor.new.exe.failed" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe.failed"`+"\r\n"+`start "" "C:\data\sensorium-supervisor.exe"`) {
84
- t.Fatalf("task-mode applied restart block missing\nscript:\n%s", script)
85
- }
86
- }
87
-
88
- func TestBuildWindowsApplyHelperScript_ServiceModeDoesNotStartBinary(t *testing.T) {
89
- cfg := Config{
90
- Paths: Paths{
91
- PendingBinary: `C:\data\bin\sensorium-supervisor.new.exe`,
92
- PendingVersion: `C:\data\bin\sensorium-supervisor.new.exe.version`,
93
- SupervisorVersion: `C:\data\supervisor-version.txt`,
94
- },
95
- }
96
- exePath := `C:\data\sensorium-supervisor.exe`
97
-
98
- script := buildWindowsApplyHelperScript(cfg, exePath, true)
99
- if strings.Contains(script, `start "" "C:\data\sensorium-supervisor.exe"`) {
100
- t.Fatalf("service-mode helper unexpectedly starts supervisor binary\nscript:\n%s", script)
101
- }
102
-
103
- serviceFailRestart := `:fail` + "\r\n" + `set "FAIL_REASON=helper failed to swap pending supervisor binary after retries (pending=C:\data\bin\sensorium-supervisor.new.exe current=C:\data\sensorium-supervisor.exe)"` + "\r\n" + `<nul set /p "=%FAIL_REASON%" > "C:\data\bin\sensorium-supervisor.new.exe.failed"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe"` + "\r\n" + `if exist "C:\data\bin\sensorium-supervisor.new.exe.version" del /F /Q "C:\data\bin\sensorium-supervisor.new.exe.version"` + "\r\n" + `sc start "SensoriumSupervisor" >NUL 2>&1` + "\r\n" + `if errorlevel 1 (` + "\r\n" + ` timeout /T 2 /NOBREAK >NUL` + "\r\n" + ` sc start "SensoriumSupervisor" >NUL 2>&1` + "\r\n" + `)`
104
- if !strings.Contains(script, serviceFailRestart) {
105
- t.Fatalf("service-mode fail recovery assist block missing\nscript:\n%s", script)
106
- }
107
- }
108
-
109
- func TestBuildWindowsApplyHelperScript_EscapesFailReasonForCmdSafety(t *testing.T) {
110
- cfg := Config{
111
- Paths: Paths{
112
- PendingBinary: `C:\data\bin\sensorium&supervisor.new%TMP%.exe`,
113
- PendingVersion: `C:\data\bin\sensorium-supervisor.new.exe.version`,
114
- SupervisorVersion: `C:\data\supervisor-version.txt`,
115
- },
116
- }
117
- exePath := `C:\data\sensorium"supervisor.exe`
118
-
119
- script := buildWindowsApplyHelperScript(cfg, exePath, false)
120
-
121
- if !strings.Contains(script, `set "FAIL_REASON=helper failed to swap pending supervisor binary after retries (pending=C:\data\bin\sensorium&supervisor.new%%TMP%%.exe current=C:\data\sensorium'supervisor.exe)"`) {
122
- t.Fatalf("expected escaped quoted FAIL_REASON assignment\nscript:\n%s", script)
123
- }
124
- if strings.Contains(script, `set FAIL_REASON=`) {
125
- t.Fatalf("found unsafe unquoted FAIL_REASON assignment\nscript:\n%s", script)
126
- }
127
- }
128
-
129
- func TestRecordPendingSupervisorApplyFailureIfPresent_IgnoresStaleMarkerWhenTargetAlreadyApplied(t *testing.T) {
130
- dir := t.TempDir()
131
- log := NewLogger(filepath.Join(dir, "test.log"))
132
- defer log.Close()
133
-
134
- targetVersion := "2.0.0"
135
- cfg := Config{
136
- Paths: Paths{
137
- PendingBinary: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe"),
138
- PendingVersion: filepath.Join(dir, "bin", "sensorium-supervisor.new.exe.version"),
139
- SupervisorVersion: filepath.Join(dir, "supervisor-version.txt"),
140
- UpdateState: filepath.Join(dir, "update-state.json"),
141
- },
142
- }
143
-
144
- if err := os.MkdirAll(filepath.Dir(cfg.Paths.PendingBinary), 0755); err != nil {
145
- t.Fatalf("create pending dir: %v", err)
146
- }
147
- if err := os.WriteFile(cfg.Paths.SupervisorVersion, []byte(targetVersion), 0644); err != nil {
148
- t.Fatalf("write supervisor version: %v", err)
149
- }
150
-
151
- store := NewUpdateStateStore(cfg.Paths.UpdateState, log)
152
- store.Transition(updateScopeSupervisor, updatePhaseFailed, targetVersion, "1.0.0", "old failure")
153
-
154
- if err := os.WriteFile(supervisorApplyFailureMarkerPath(cfg), []byte("stale helper failure"), 0644); err != nil {
155
- t.Fatalf("write apply failure marker: %v", err)
156
- }
157
-
158
- recordPendingSupervisorApplyFailureIfPresent(cfg, log)
159
-
160
- state, err := store.Load()
161
- if err != nil {
162
- t.Fatalf("load state: %v", err)
163
- }
164
- if state.Phase != updatePhaseIdle {
165
- t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseIdle)
166
- }
167
- if state.TargetVersion != targetVersion {
168
- t.Fatalf("target = %q, want %q", state.TargetVersion, targetVersion)
169
- }
170
- if state.LastError != "" {
171
- t.Fatalf("last error = %q, want empty", state.LastError)
172
- }
173
-
174
- if _, statErr := os.Stat(supervisorApplyFailureMarkerPath(cfg)); !os.IsNotExist(statErr) {
175
- t.Fatalf("expected apply failure marker to be removed, got: %v", statErr)
176
- }
177
- }
@@ -1,9 +0,0 @@
1
- //go:build !windows
2
-
3
- package main
4
-
5
- import "errors"
6
-
7
- func scheduleServiceRestartForUpdate(_ *Logger) error {
8
- return errors.New("not supported on this OS")
9
- }