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.
- package/Install-Sensorium.ps1 +327 -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 -27
- 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 -555
- package/dist/data/memory/consolidation.js.map +1 -1
- package/dist/data/memory/index.d.ts +0 -1
- package/dist/data/memory/index.d.ts.map +1 -1
- package/dist/data/memory/index.js +0 -1
- package/dist/data/memory/index.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/semantic.d.ts +0 -1
- package/dist/data/memory/semantic.d.ts.map +1 -1
- package/dist/data/memory/semantic.js +2 -8
- package/dist/data/memory/semantic.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/defs/memory-defs.d.ts.map +1 -1
- package/dist/tools/defs/memory-defs.js +0 -19
- package/dist/tools/defs/memory-defs.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 -16
- 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 +24 -42
- 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 +66 -106
- 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 -1163
- 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 +60 -11
- package/supervisor/health_test.go +29 -0
- package/supervisor/keeper.go +15 -10
- package/supervisor/log.go +109 -28
- package/supervisor/log_test.go +86 -6
- package/supervisor/main.go +150 -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 +311 -10
- package/supervisor/updater_test.go +64 -0
- package/dist/data/memory/quality-scoring.d.ts +0 -32
- package/dist/data/memory/quality-scoring.d.ts.map +0 -1
- package/dist/data/memory/quality-scoring.js +0 -182
- package/dist/data/memory/quality-scoring.js.map +0 -1
- 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/templates/coding-task.default.md +0 -12
package/supervisor/updater.go
CHANGED
|
@@ -4,21 +4,31 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"encoding/json"
|
|
6
6
|
"fmt"
|
|
7
|
+
"io"
|
|
7
8
|
"net/http"
|
|
8
9
|
"os"
|
|
9
10
|
"path/filepath"
|
|
10
11
|
"runtime"
|
|
11
12
|
"strings"
|
|
13
|
+
"syscall"
|
|
12
14
|
"time"
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
const registryURL = "https://registry.npmjs.org/sensorium-mcp/latest"
|
|
18
|
+
const supervisorReleaseURL = "https://api.github.com/repos/andriyshevchenko/remote-copilot-mcp/releases/tags/supervisor-latest"
|
|
19
|
+
|
|
20
|
+
var (
|
|
21
|
+
notifyUpdaterOperator = NotifyOperator
|
|
22
|
+
mcpUpdateReadyPollInterval = 3 * time.Second
|
|
23
|
+
mcpUpdateReadyTimeout = 60 * time.Second
|
|
24
|
+
)
|
|
16
25
|
|
|
17
26
|
// Updater checks the npm registry for new versions and performs updates.
|
|
18
27
|
type Updater struct {
|
|
19
28
|
cfg Config
|
|
20
29
|
mcp *MCPClient
|
|
21
30
|
log *Logger
|
|
31
|
+
state *UpdateStateStore
|
|
22
32
|
startAt time.Time
|
|
23
33
|
cancel context.CancelFunc
|
|
24
34
|
done chan struct{}
|
|
@@ -29,6 +39,7 @@ func NewUpdater(cfg Config, mcp *MCPClient, log *Logger) *Updater {
|
|
|
29
39
|
cfg: cfg,
|
|
30
40
|
mcp: mcp,
|
|
31
41
|
log: log,
|
|
42
|
+
state: NewUpdateStateStore(cfg.Paths.UpdateState, log),
|
|
32
43
|
startAt: time.Now(),
|
|
33
44
|
done: make(chan struct{}),
|
|
34
45
|
}
|
|
@@ -75,6 +86,10 @@ func (u *Updater) run(ctx context.Context) {
|
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
u.checkAndUpdate(ctx)
|
|
89
|
+
if ctx.Err() != nil {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
u.checkSupervisorUpdate(ctx)
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
|
|
@@ -159,30 +174,54 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
|
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
u.log.Info("Update available: %s → %s", local, remote)
|
|
162
|
-
|
|
177
|
+
coordLock, ok := AcquireUpdateCoordinatorLock(u.cfg.Paths.UpdateApplyLock, updateScopeMCP, u.log)
|
|
178
|
+
if !ok {
|
|
179
|
+
u.log.Info("Deferring MCP update %s → %s due to active update apply lock", local, remote)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
defer coordLock.Release()
|
|
183
|
+
|
|
184
|
+
u.state.Transition(updateScopeMCP, updatePhaseApplying, remote, local, "")
|
|
185
|
+
markFailed := func(err error) {
|
|
186
|
+
u.state.Transition(updateScopeMCP, updatePhaseFailed, remote, local, err.Error())
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating sensorium v%s → v%s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
|
|
163
190
|
|
|
164
191
|
// Grace period
|
|
165
192
|
u.log.Info("Grace period %v...", u.cfg.GracePeriod)
|
|
166
193
|
select {
|
|
167
194
|
case <-ctx.Done():
|
|
195
|
+
markFailed(ctx.Err())
|
|
168
196
|
return
|
|
169
197
|
case <-time.After(u.cfg.GracePeriod):
|
|
170
198
|
}
|
|
171
199
|
|
|
172
|
-
// Set maintenance flag — always clean up on exit
|
|
173
|
-
|
|
200
|
+
// Set maintenance flag — always clean up on exit.
|
|
201
|
+
// Written as JSON so TypeScript's checkMaintenanceFlag() can parse the
|
|
202
|
+
// version and timestamp fields for accurate maintenance notifications.
|
|
203
|
+
maintenanceJSON, err := json.Marshal(map[string]string{
|
|
204
|
+
"version": remote,
|
|
205
|
+
"timestamp": time.Now().Format(time.RFC3339),
|
|
206
|
+
})
|
|
207
|
+
if err != nil {
|
|
208
|
+
u.log.Warn("Failed to marshal maintenance flag: %v", err)
|
|
209
|
+
} else if err := atomicWrite(u.cfg.Paths.MaintenanceFlag, maintenanceJSON); err != nil {
|
|
174
210
|
u.log.Warn("Failed to write maintenance flag: %v", err)
|
|
175
211
|
}
|
|
176
212
|
defer os.Remove(u.cfg.Paths.MaintenanceFlag)
|
|
177
213
|
|
|
178
214
|
// Kill the current MCP server
|
|
179
215
|
if ctx.Err() != nil {
|
|
216
|
+
markFailed(ctx.Err())
|
|
180
217
|
return
|
|
181
218
|
}
|
|
219
|
+
u.state.Transition(updateScopeMCP, updatePhaseRestarting, remote, local, "")
|
|
182
220
|
u.killServer()
|
|
183
221
|
|
|
184
222
|
// Clean npx cache
|
|
185
223
|
if ctx.Err() != nil {
|
|
224
|
+
markFailed(ctx.Err())
|
|
186
225
|
return
|
|
187
226
|
}
|
|
188
227
|
u.clearNpxCache()
|
|
@@ -191,6 +230,7 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
|
|
|
191
230
|
var pid int
|
|
192
231
|
for attempt := 1; attempt <= 3; attempt++ {
|
|
193
232
|
if ctx.Err() != nil {
|
|
233
|
+
markFailed(ctx.Err())
|
|
194
234
|
return
|
|
195
235
|
}
|
|
196
236
|
pid, err = SpawnMCPServer(u.cfg, u.log)
|
|
@@ -204,26 +244,287 @@ func (u *Updater) checkAndUpdate(ctx context.Context) {
|
|
|
204
244
|
}
|
|
205
245
|
if err != nil {
|
|
206
246
|
u.log.Error("All spawn attempts failed — server is down!")
|
|
207
|
-
|
|
247
|
+
markFailed(err)
|
|
248
|
+
notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update FAILED — server is down! Manual intervention required.", 0)
|
|
208
249
|
return
|
|
209
250
|
}
|
|
210
251
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
u.log.Info("Updated MCP server ready (PID %d)", pid)
|
|
214
|
-
} else {
|
|
215
|
-
u.log.Warn("Updated server did not become ready in 60s")
|
|
252
|
+
if !u.verifyUpdatedMCPServerReady(ctx, remote, local, pid) {
|
|
253
|
+
return
|
|
216
254
|
}
|
|
217
255
|
|
|
218
256
|
u.setLocalVersion(remote)
|
|
257
|
+
u.state.Transition(updateScopeMCP, updatePhaseIdle, remote, local, "")
|
|
219
258
|
|
|
220
|
-
|
|
259
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("✅ Supervisor: update to v%s complete. Server ready.", remote), 0)
|
|
221
260
|
u.log.Info("Update complete: v%s → v%s", local, remote)
|
|
222
261
|
|
|
223
262
|
// Reset start time for min uptime tracking
|
|
224
263
|
u.startAt = time.Now()
|
|
225
264
|
}
|
|
226
265
|
|
|
266
|
+
func (u *Updater) verifyUpdatedMCPServerReady(ctx context.Context, remote, local string, pid int) bool {
|
|
267
|
+
u.state.Transition(updateScopeMCP, updatePhaseVerifying, remote, local, "")
|
|
268
|
+
if u.mcp.WaitForReady(ctx, mcpUpdateReadyPollInterval, mcpUpdateReadyTimeout) {
|
|
269
|
+
u.log.Info("Updated MCP server ready (PID %d)", pid)
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
errMsg := fmt.Sprintf("updated MCP server did not become ready within %v after restart (pid=%d)", mcpUpdateReadyTimeout, pid)
|
|
274
|
+
u.log.Error(errMsg)
|
|
275
|
+
u.state.Transition(updateScopeMCP, updatePhaseFailed, remote, local, errMsg)
|
|
276
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: update to v%s FAILED verification. Server did not become ready after restart.", remote), 0)
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type githubRelease struct {
|
|
281
|
+
TagName string `json:"tag_name"`
|
|
282
|
+
Name string `json:"name"`
|
|
283
|
+
Assets []struct {
|
|
284
|
+
Name string `json:"name"`
|
|
285
|
+
URL string `json:"browser_download_url"`
|
|
286
|
+
Size int64 `json:"size"`
|
|
287
|
+
} `json:"assets"`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
func (u *Updater) getSupervisorRelease(ctx context.Context) (string, string, error) {
|
|
291
|
+
ctx2, cancel := context.WithTimeout(ctx, 20*time.Second)
|
|
292
|
+
defer cancel()
|
|
293
|
+
|
|
294
|
+
req, err := http.NewRequestWithContext(ctx2, http.MethodGet, supervisorReleaseURL, nil)
|
|
295
|
+
if err != nil {
|
|
296
|
+
return "", "", err
|
|
297
|
+
}
|
|
298
|
+
req.Header.Set("Accept", "application/vnd.github+json")
|
|
299
|
+
req.Header.Set("User-Agent", "sensorium-supervisor-updater")
|
|
300
|
+
|
|
301
|
+
resp, err := http.DefaultClient.Do(req)
|
|
302
|
+
if err != nil {
|
|
303
|
+
return "", "", err
|
|
304
|
+
}
|
|
305
|
+
defer resp.Body.Close()
|
|
306
|
+
|
|
307
|
+
if resp.StatusCode != http.StatusOK {
|
|
308
|
+
return "", "", fmt.Errorf("GitHub releases HTTP %d", resp.StatusCode)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
var release githubRelease
|
|
312
|
+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
313
|
+
return "", "", err
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
assetName := supervisorAssetName()
|
|
317
|
+
for _, asset := range release.Assets {
|
|
318
|
+
if asset.Name != assetName {
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
version := strings.TrimSpace(release.Name)
|
|
323
|
+
if version == "" {
|
|
324
|
+
version = strings.TrimSpace(release.TagName)
|
|
325
|
+
}
|
|
326
|
+
if version == "" {
|
|
327
|
+
return "", "", fmt.Errorf("release version missing for %s", assetName)
|
|
328
|
+
}
|
|
329
|
+
if strings.TrimSpace(asset.URL) == "" {
|
|
330
|
+
return "", "", fmt.Errorf("release asset URL missing for %s", assetName)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return version, asset.URL, nil
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return "", "", fmt.Errorf("release asset %q not found", assetName)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func supervisorAssetName() string {
|
|
340
|
+
suffix := ""
|
|
341
|
+
if runtime.GOOS == "windows" {
|
|
342
|
+
suffix = ".exe"
|
|
343
|
+
}
|
|
344
|
+
return fmt.Sprintf("sensorium-supervisor-%s-%s%s", runtime.GOOS, runtime.GOARCH, suffix)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
func (u *Updater) getLocalSupervisorVersion() string {
|
|
348
|
+
data, err := os.ReadFile(u.cfg.Paths.SupervisorVersion)
|
|
349
|
+
if err != nil {
|
|
350
|
+
return ""
|
|
351
|
+
}
|
|
352
|
+
return strings.TrimSpace(string(data))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func (u *Updater) setLocalSupervisorVersion(v string) {
|
|
356
|
+
os.MkdirAll(u.cfg.DataDir, 0755)
|
|
357
|
+
if err := atomicWrite(u.cfg.Paths.SupervisorVersion, []byte(v)); err != nil {
|
|
358
|
+
u.log.Warn("Failed to write supervisor version file: %v", err)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func (u *Updater) stagePendingSupervisorVersion(v string) error {
|
|
363
|
+
if err := os.MkdirAll(filepath.Dir(u.cfg.Paths.PendingVersion), 0755); err != nil {
|
|
364
|
+
return fmt.Errorf("create pending supervisor version dir: %w", err)
|
|
365
|
+
}
|
|
366
|
+
if err := atomicWrite(u.cfg.Paths.PendingVersion, []byte(v)); err != nil {
|
|
367
|
+
return fmt.Errorf("write pending supervisor version: %w", err)
|
|
368
|
+
}
|
|
369
|
+
return nil
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
func (u *Updater) checkSupervisorUpdate(ctx context.Context) {
|
|
373
|
+
uptime := time.Since(u.startAt)
|
|
374
|
+
if uptime < u.cfg.MinUptime {
|
|
375
|
+
u.log.Info("Deferring supervisor update — too early (uptime %v < %v)", uptime.Round(time.Second), u.cfg.MinUptime)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
remote, downloadURL, err := u.getSupervisorRelease(ctx)
|
|
380
|
+
if err != nil {
|
|
381
|
+
u.log.Warn("Failed to check supervisor release: %v", err)
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
local := u.getLocalSupervisorVersion()
|
|
386
|
+
if local == "" {
|
|
387
|
+
u.log.Info("No local supervisor version recorded — storing %s", remote)
|
|
388
|
+
u.setLocalSupervisorVersion(remote)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if local == remote {
|
|
393
|
+
u.log.Debug("Supervisor updater: version %s is up to date", local)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
u.log.Info("Supervisor update available: %s → %s", local, remote)
|
|
398
|
+
coordLock, ok := AcquireUpdateCoordinatorLock(u.cfg.Paths.UpdateApplyLock, updateScopeSupervisor, u.log)
|
|
399
|
+
if !ok {
|
|
400
|
+
u.log.Info("Deferring supervisor binary update %s → %s due to active update apply lock", local, remote)
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
defer coordLock.Release()
|
|
404
|
+
|
|
405
|
+
markFailed := func(err error) {
|
|
406
|
+
u.state.Transition(updateScopeSupervisor, updatePhaseFailed, remote, local, err.Error())
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: updating binary %s → %s. Grace period %v...", local, remote, u.cfg.GracePeriod), 0)
|
|
410
|
+
|
|
411
|
+
select {
|
|
412
|
+
case <-ctx.Done():
|
|
413
|
+
markFailed(ctx.Err())
|
|
414
|
+
return
|
|
415
|
+
case <-time.After(u.cfg.GracePeriod):
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if err := u.downloadSupervisorBinary(ctx, downloadURL); err != nil {
|
|
419
|
+
markFailed(err)
|
|
420
|
+
u.log.Error("Supervisor binary download failed: %v", err)
|
|
421
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: binary update to %s failed during download.", remote), 0)
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if err := u.stagePendingSupervisorVersion(remote); err != nil {
|
|
426
|
+
_ = os.Remove(u.cfg.Paths.PendingBinary)
|
|
427
|
+
markFailed(err)
|
|
428
|
+
u.log.Error("Failed to stage supervisor version %s: %v", remote, err)
|
|
429
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("🔴 Supervisor: binary update to %s failed during staging.", remote), 0)
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
u.state.Transition(updateScopeSupervisor, updatePhaseStaged, remote, local, "")
|
|
433
|
+
notifyUpdaterOperator(u.cfg, u.log, fmt.Sprintf("⚙️ Supervisor: downloaded %s. Restarting supervisor to apply update...", remote), 0)
|
|
434
|
+
|
|
435
|
+
isService, err := isWindowsService()
|
|
436
|
+
if err != nil {
|
|
437
|
+
markFailed(err)
|
|
438
|
+
u.log.Error("Failed to detect service mode for restart: %v", err)
|
|
439
|
+
notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but service detection failed.", 0)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
u.state.Transition(updateScopeSupervisor, updatePhaseRestarting, remote, local, "")
|
|
443
|
+
|
|
444
|
+
if isService {
|
|
445
|
+
if err := scheduleServiceRestartForUpdate(u.log); err != nil {
|
|
446
|
+
markFailed(err)
|
|
447
|
+
u.log.Error("Failed to schedule service restart: %v", err)
|
|
448
|
+
notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but service restart scheduling failed.", 0)
|
|
449
|
+
}
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if err := signalSelf(syscall.SIGTERM); err != nil {
|
|
454
|
+
markFailed(err)
|
|
455
|
+
u.log.Error("Failed to signal supervisor for restart: %v", err)
|
|
456
|
+
notifyUpdaterOperator(u.cfg, u.log, "🔴 Supervisor: update downloaded but restart signal failed.", 0)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
func (u *Updater) downloadSupervisorBinary(ctx context.Context, downloadURL string) error {
|
|
461
|
+
if err := os.MkdirAll(u.cfg.Paths.BinaryDir, 0755); err != nil {
|
|
462
|
+
return fmt.Errorf("create binary dir: %w", err)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
tmpPath := u.cfg.Paths.PendingBinary + ".download"
|
|
466
|
+
defer os.Remove(tmpPath)
|
|
467
|
+
|
|
468
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
|
|
469
|
+
if err != nil {
|
|
470
|
+
return err
|
|
471
|
+
}
|
|
472
|
+
req.Header.Set("User-Agent", "sensorium-supervisor-updater")
|
|
473
|
+
|
|
474
|
+
resp, err := http.DefaultClient.Do(req)
|
|
475
|
+
if err != nil {
|
|
476
|
+
return err
|
|
477
|
+
}
|
|
478
|
+
defer resp.Body.Close()
|
|
479
|
+
|
|
480
|
+
if resp.StatusCode != http.StatusOK {
|
|
481
|
+
return fmt.Errorf("download HTTP %d", resp.StatusCode)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
|
|
485
|
+
if err != nil {
|
|
486
|
+
return err
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
written, copyErr := io.Copy(f, resp.Body)
|
|
490
|
+
closeErr := f.Close()
|
|
491
|
+
if copyErr != nil {
|
|
492
|
+
return copyErr
|
|
493
|
+
}
|
|
494
|
+
if closeErr != nil {
|
|
495
|
+
return closeErr
|
|
496
|
+
}
|
|
497
|
+
if written <= 0 {
|
|
498
|
+
return fmt.Errorf("downloaded empty binary")
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
info, err := os.Stat(tmpPath)
|
|
502
|
+
if err != nil {
|
|
503
|
+
return err
|
|
504
|
+
}
|
|
505
|
+
if info.Size() <= 0 {
|
|
506
|
+
return fmt.Errorf("downloaded binary has invalid size %d", info.Size())
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if err := os.Remove(u.cfg.Paths.PendingBinary); err != nil && !os.IsNotExist(err) {
|
|
510
|
+
return err
|
|
511
|
+
}
|
|
512
|
+
if err := os.Rename(tmpPath, u.cfg.Paths.PendingBinary); err != nil {
|
|
513
|
+
return err
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
u.log.Info("Supervisor binary downloaded to %s (%d bytes)", u.cfg.Paths.PendingBinary, info.Size())
|
|
517
|
+
return nil
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
func signalSelf(sig os.Signal) error {
|
|
521
|
+
proc, err := os.FindProcess(os.Getpid())
|
|
522
|
+
if err != nil {
|
|
523
|
+
return err
|
|
524
|
+
}
|
|
525
|
+
return proc.Signal(sig)
|
|
526
|
+
}
|
|
527
|
+
|
|
227
528
|
func (u *Updater) killServer() {
|
|
228
529
|
u.log.Info("Updater: stopping current MCP server for update")
|
|
229
530
|
pid, err := ReadPIDFile(u.cfg.Paths.ServerPID)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestVerifyUpdatedMCPServerReady_FailureSetsFailedStateAndNoSuccessMessage(t *testing.T) {
|
|
12
|
+
dir := t.TempDir()
|
|
13
|
+
log := NewLogger(filepath.Join(dir, "test.log"))
|
|
14
|
+
defer log.Close()
|
|
15
|
+
|
|
16
|
+
cfg := Config{
|
|
17
|
+
DataDir: dir,
|
|
18
|
+
Paths: Paths{
|
|
19
|
+
UpdateState: filepath.Join(dir, "update-state.json"),
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
u := NewUpdater(cfg, NewMCPClient(1, ""), log)
|
|
24
|
+
u.state = NewUpdateStateStore(cfg.Paths.UpdateState, log)
|
|
25
|
+
|
|
26
|
+
origNotify := notifyUpdaterOperator
|
|
27
|
+
origPoll := mcpUpdateReadyPollInterval
|
|
28
|
+
origTimeout := mcpUpdateReadyTimeout
|
|
29
|
+
defer func() {
|
|
30
|
+
notifyUpdaterOperator = origNotify
|
|
31
|
+
mcpUpdateReadyPollInterval = origPoll
|
|
32
|
+
mcpUpdateReadyTimeout = origTimeout
|
|
33
|
+
}()
|
|
34
|
+
|
|
35
|
+
mcpUpdateReadyPollInterval = 1 * time.Millisecond
|
|
36
|
+
mcpUpdateReadyTimeout = 5 * time.Millisecond
|
|
37
|
+
|
|
38
|
+
var messages []string
|
|
39
|
+
notifyUpdaterOperator = func(_ Config, _ *Logger, text string, _ int) {
|
|
40
|
+
messages = append(messages, text)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ok := u.verifyUpdatedMCPServerReady(context.Background(), "2.0.0", "1.0.0", 4242)
|
|
44
|
+
if ok {
|
|
45
|
+
t.Fatal("expected verification to fail")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
state, err := u.state.Load()
|
|
49
|
+
if err != nil {
|
|
50
|
+
t.Fatalf("load update state: %v", err)
|
|
51
|
+
}
|
|
52
|
+
if state.Phase != updatePhaseFailed {
|
|
53
|
+
t.Fatalf("state phase = %q, want %q", state.Phase, updatePhaseFailed)
|
|
54
|
+
}
|
|
55
|
+
if !strings.Contains(state.LastError, "did not become ready") {
|
|
56
|
+
t.Fatalf("last error = %q, want readiness failure detail", state.LastError)
|
|
57
|
+
}
|
|
58
|
+
if len(messages) == 0 {
|
|
59
|
+
t.Fatal("expected failure notification message")
|
|
60
|
+
}
|
|
61
|
+
if strings.Contains(messages[len(messages)-1], "complete") {
|
|
62
|
+
t.Fatalf("unexpected success message: %q", messages[len(messages)-1])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory quality scoring — samples random active notes and scores them
|
|
3
|
-
* via LLM on specificity, actionability, and accuracy.
|
|
4
|
-
*
|
|
5
|
-
* Results are persisted in a `quality_scores` table for trend tracking.
|
|
6
|
-
*/
|
|
7
|
-
import type { Database } from "./schema.js";
|
|
8
|
-
interface NoteScore {
|
|
9
|
-
noteId: string;
|
|
10
|
-
content: string;
|
|
11
|
-
specificity: number;
|
|
12
|
-
actionability: number;
|
|
13
|
-
accuracy: number;
|
|
14
|
-
avg: number;
|
|
15
|
-
}
|
|
16
|
-
export interface QualityScoreResult {
|
|
17
|
-
id: string;
|
|
18
|
-
scoredAt: string;
|
|
19
|
-
sampleSize: number;
|
|
20
|
-
avgSpecificity: number;
|
|
21
|
-
avgActionability: number;
|
|
22
|
-
avgAccuracy: number;
|
|
23
|
-
overallAvg: number;
|
|
24
|
-
details: NoteScore[];
|
|
25
|
-
}
|
|
26
|
-
export interface ScoreOptions {
|
|
27
|
-
sampleSize?: number;
|
|
28
|
-
}
|
|
29
|
-
export declare function scoreMemoryQuality(db: Database, options?: ScoreOptions): Promise<QualityScoreResult>;
|
|
30
|
-
export declare function formatScoreComparison(db: Database, current: QualityScoreResult): string;
|
|
31
|
-
export {};
|
|
32
|
-
//# sourceMappingURL=quality-scoring.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"quality-scoring.d.ts","sourceRoot":"","sources":["../../../src/data/memory/quality-scoring.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAiB5C,UAAU,SAAS;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAwJD,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,QAAQ,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAmD7B;AAID,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,kBAAkB,GAC1B,MAAM,CAqCR"}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory quality scoring — samples random active notes and scores them
|
|
3
|
-
* via LLM on specificity, actionability, and accuracy.
|
|
4
|
-
*
|
|
5
|
-
* Results are persisted in a `quality_scores` table for trend tracking.
|
|
6
|
-
*/
|
|
7
|
-
import { generateId, nowISO, repairAndParseJSON } from "./utils.js";
|
|
8
|
-
import { chatCompletion } from "../../integrations/openai/chat.js";
|
|
9
|
-
import { log } from "../../logger.js";
|
|
10
|
-
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
11
|
-
const DEFAULT_SAMPLE_SIZE = 20;
|
|
12
|
-
const SCORING_MODEL_ENV = "QUALITY_SCORING_MODEL";
|
|
13
|
-
const FALLBACK_MODEL_ENV = "CONSOLIDATION_MODEL";
|
|
14
|
-
const DEFAULT_MODEL = "gpt-4o-mini";
|
|
15
|
-
const MAX_TOKENS = 2048;
|
|
16
|
-
const TIMEOUT_MS = 60_000;
|
|
17
|
-
// ─── Schema ──────────────────────────────────────────────────────────────────
|
|
18
|
-
function ensureQualityScoresTable(db) {
|
|
19
|
-
db.exec(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS quality_scores (
|
|
21
|
-
id TEXT PRIMARY KEY,
|
|
22
|
-
scored_at TEXT NOT NULL,
|
|
23
|
-
sample_size INTEGER NOT NULL,
|
|
24
|
-
avg_specificity REAL,
|
|
25
|
-
avg_actionability REAL,
|
|
26
|
-
avg_accuracy REAL,
|
|
27
|
-
overall_avg REAL,
|
|
28
|
-
details TEXT
|
|
29
|
-
)
|
|
30
|
-
`);
|
|
31
|
-
}
|
|
32
|
-
// ─── LLM Prompt ──────────────────────────────────────────────────────────────
|
|
33
|
-
function buildScoringPrompt(notes) {
|
|
34
|
-
const notesList = notes
|
|
35
|
-
.map((n, i) => `${i + 1}. [${n.noteId}] ${n.content}`)
|
|
36
|
-
.join("\n");
|
|
37
|
-
return [
|
|
38
|
-
{
|
|
39
|
-
role: "system",
|
|
40
|
-
content: [
|
|
41
|
-
"You are a memory quality auditor. Score each note on three dimensions (1-5 each):",
|
|
42
|
-
"- **Specificity**: 1=vague platitude, 5=precise actionable fact",
|
|
43
|
-
"- **Actionability**: 1=useless to a future agent, 5=directly actionable",
|
|
44
|
-
"- **Accuracy**: 1=garbled/contradictory, 5=clear and coherent",
|
|
45
|
-
"",
|
|
46
|
-
"Respond ONLY with valid JSON: { \"scores\": [ { \"noteId\": \"...\", \"specificity\": N, \"actionability\": N, \"accuracy\": N } ] }",
|
|
47
|
-
].join("\n"),
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
role: "user",
|
|
51
|
-
content: `Score these ${notes.length} memory notes:\n\n${notesList}`,
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
// ─── Sampling ────────────────────────────────────────────────────────────────
|
|
56
|
-
function sampleActiveNotes(db, count) {
|
|
57
|
-
const rows = db.prepare(`SELECT note_id AS noteId, content
|
|
58
|
-
FROM semantic_notes
|
|
59
|
-
WHERE valid_to IS NULL AND superseded_by IS NULL
|
|
60
|
-
ORDER BY RANDOM()
|
|
61
|
-
LIMIT ?`).all(count);
|
|
62
|
-
return rows;
|
|
63
|
-
}
|
|
64
|
-
// ─── Persistence ─────────────────────────────────────────────────────────────
|
|
65
|
-
function saveScoreResult(db, result) {
|
|
66
|
-
db.prepare(`INSERT INTO quality_scores (id, scored_at, sample_size, avg_specificity, avg_actionability, avg_accuracy, overall_avg, details)
|
|
67
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(result.id, result.scoredAt, result.sampleSize, result.avgSpecificity, result.avgActionability, result.avgAccuracy, result.overallAvg, JSON.stringify(result.details));
|
|
68
|
-
}
|
|
69
|
-
function getLastRun(db) {
|
|
70
|
-
const row = db.prepare(`SELECT id, scored_at, overall_avg
|
|
71
|
-
FROM quality_scores
|
|
72
|
-
ORDER BY scored_at DESC
|
|
73
|
-
LIMIT 1`).get();
|
|
74
|
-
return row;
|
|
75
|
-
}
|
|
76
|
-
// ─── Aggregation ─────────────────────────────────────────────────────────────
|
|
77
|
-
function aggregateScores(notes, rawScores) {
|
|
78
|
-
const scoreMap = new Map(rawScores.map(s => [s.noteId, s]));
|
|
79
|
-
return notes
|
|
80
|
-
.filter(n => scoreMap.has(n.noteId))
|
|
81
|
-
.map(n => {
|
|
82
|
-
const s = scoreMap.get(n.noteId);
|
|
83
|
-
return {
|
|
84
|
-
noteId: n.noteId,
|
|
85
|
-
content: n.content,
|
|
86
|
-
specificity: clampScore(s.specificity),
|
|
87
|
-
actionability: clampScore(s.actionability),
|
|
88
|
-
accuracy: clampScore(s.accuracy),
|
|
89
|
-
avg: round((clampScore(s.specificity) + clampScore(s.actionability) + clampScore(s.accuracy)) / 3),
|
|
90
|
-
};
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
function clampScore(v) {
|
|
94
|
-
return Math.max(1, Math.min(5, Math.round(v)));
|
|
95
|
-
}
|
|
96
|
-
function round(v) {
|
|
97
|
-
return Math.round(v * 100) / 100;
|
|
98
|
-
}
|
|
99
|
-
function computeAverages(details) {
|
|
100
|
-
if (details.length === 0) {
|
|
101
|
-
return { avgSpecificity: 0, avgActionability: 0, avgAccuracy: 0, overallAvg: 0 };
|
|
102
|
-
}
|
|
103
|
-
const sum = details.reduce((acc, d) => ({
|
|
104
|
-
s: acc.s + d.specificity,
|
|
105
|
-
a: acc.a + d.actionability,
|
|
106
|
-
c: acc.c + d.accuracy,
|
|
107
|
-
}), { s: 0, a: 0, c: 0 });
|
|
108
|
-
const n = details.length;
|
|
109
|
-
const avgSpecificity = round(sum.s / n);
|
|
110
|
-
const avgActionability = round(sum.a / n);
|
|
111
|
-
const avgAccuracy = round(sum.c / n);
|
|
112
|
-
const overallAvg = round((avgSpecificity + avgActionability + avgAccuracy) / 3);
|
|
113
|
-
return { avgSpecificity, avgActionability, avgAccuracy, overallAvg };
|
|
114
|
-
}
|
|
115
|
-
export async function scoreMemoryQuality(db, options) {
|
|
116
|
-
ensureQualityScoresTable(db);
|
|
117
|
-
const sampleSize = options?.sampleSize ?? DEFAULT_SAMPLE_SIZE;
|
|
118
|
-
const notes = sampleActiveNotes(db, sampleSize);
|
|
119
|
-
if (notes.length === 0) {
|
|
120
|
-
throw new Error("No active semantic notes to score.");
|
|
121
|
-
}
|
|
122
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
123
|
-
if (!apiKey) {
|
|
124
|
-
throw new Error("OPENAI_API_KEY not set — cannot score memory quality.");
|
|
125
|
-
}
|
|
126
|
-
const model = process.env[SCORING_MODEL_ENV] ??
|
|
127
|
-
process.env[FALLBACK_MODEL_ENV] ??
|
|
128
|
-
DEFAULT_MODEL;
|
|
129
|
-
const messages = buildScoringPrompt(notes);
|
|
130
|
-
const raw = await chatCompletion(messages, apiKey, {
|
|
131
|
-
model,
|
|
132
|
-
maxTokens: MAX_TOKENS,
|
|
133
|
-
responseFormat: { type: "json_object" },
|
|
134
|
-
timeoutMs: TIMEOUT_MS,
|
|
135
|
-
});
|
|
136
|
-
const parsed = repairAndParseJSON(raw);
|
|
137
|
-
if (!parsed.scores || !Array.isArray(parsed.scores)) {
|
|
138
|
-
throw new Error("LLM returned invalid scoring response — missing 'scores' array.");
|
|
139
|
-
}
|
|
140
|
-
const details = aggregateScores(notes, parsed.scores);
|
|
141
|
-
const averages = computeAverages(details);
|
|
142
|
-
const result = {
|
|
143
|
-
id: generateId("qs"),
|
|
144
|
-
scoredAt: nowISO(),
|
|
145
|
-
sampleSize: notes.length,
|
|
146
|
-
...averages,
|
|
147
|
-
details,
|
|
148
|
-
};
|
|
149
|
-
saveScoreResult(db, result);
|
|
150
|
-
log.info(`[quality-scoring] Scored ${details.length} notes — overall avg: ${result.overallAvg}`);
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
// ─── Comparison Helper ───────────────────────────────────────────────────────
|
|
154
|
-
export function formatScoreComparison(db, current) {
|
|
155
|
-
ensureQualityScoresTable(db);
|
|
156
|
-
const prev = db.prepare(`SELECT id, scored_at, overall_avg
|
|
157
|
-
FROM quality_scores
|
|
158
|
-
WHERE id != ?
|
|
159
|
-
ORDER BY scored_at DESC
|
|
160
|
-
LIMIT 1`).get(current.id);
|
|
161
|
-
const lines = [
|
|
162
|
-
"## Memory Quality Score",
|
|
163
|
-
`- **Specificity**: ${current.avgSpecificity}/5`,
|
|
164
|
-
`- **Actionability**: ${current.avgActionability}/5`,
|
|
165
|
-
`- **Accuracy**: ${current.avgAccuracy}/5`,
|
|
166
|
-
`- **Overall**: ${current.overallAvg}/5`,
|
|
167
|
-
`- Sample size: ${current.sampleSize} notes`,
|
|
168
|
-
];
|
|
169
|
-
if (prev) {
|
|
170
|
-
const delta = round(current.overallAvg - prev.overall_avg);
|
|
171
|
-
const pct = prev.overall_avg > 0
|
|
172
|
-
? round((delta / prev.overall_avg) * 100)
|
|
173
|
-
: 0;
|
|
174
|
-
const direction = delta > 0 ? "improved" : delta < 0 ? "declined" : "unchanged";
|
|
175
|
-
lines.push("", `### Comparison to previous run`, `- Previous overall: ${prev.overall_avg}/5 (${prev.scored_at})`, `- Change: ${delta > 0 ? "+" : ""}${delta} (${direction} by ${Math.abs(pct)}%)`);
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
lines.push("", "_First run — no previous data for comparison._");
|
|
179
|
-
}
|
|
180
|
-
return lines.join("\n");
|
|
181
|
-
}
|
|
182
|
-
//# sourceMappingURL=quality-scoring.js.map
|