@gotcos/glasses-server 6.18.5 → 6.18.7
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/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/server/lib/managed-restart.ts +47 -0
- package/server/lib/managed-runtime.ts +3 -3
- package/server/routes/recovery.ts +17 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## 6.18.7
|
|
2
|
+
|
|
3
|
+
- **Phone Restart no longer leaves the server Stopped.** Control LaunchAgent
|
|
4
|
+
KeepAlive is `SuccessfulExit: false`, so the old SIGTERM→exit(0) path never
|
|
5
|
+
came back — phone showed "Restart requested; reconnect is taking longer than
|
|
6
|
+
expected" while Control sat at Stopped. Restart now `launchctl kickstart -k`
|
|
7
|
+
(fallback `exit(1)` so KeepAlive still fires).
|
|
8
|
+
|
|
9
|
+
## 6.18.6
|
|
10
|
+
|
|
11
|
+
- **Phone Restart works on COS Control managed installs.** Recovery status /
|
|
12
|
+
`/api/recovery/server/restart` previously required `COS_HARNESS=daemon`, but
|
|
13
|
+
Control's LaunchAgent sets `COS_MANAGED=1` with `COS_HARNESS=foreground`.
|
|
14
|
+
Health correctly advertised `managed: true` while the restart route returned
|
|
15
|
+
409 "Server is not managed by the COS LaunchAgent". Gate now uses
|
|
16
|
+
`COS_MANAGED=1` (`isManagedRuntime()`), matching capabilities.
|
|
17
|
+
|
|
1
18
|
## 6.18.5
|
|
2
19
|
|
|
3
20
|
- **Codex workspace-write now includes outbound network.** `workspace-write`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.18.
|
|
3
|
+
"version": "6.18.7",
|
|
4
4
|
"description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { userInfo } from 'node:os'
|
|
3
|
+
|
|
4
|
+
/** LaunchAgent label Control installs for the managed glasses server. */
|
|
5
|
+
export const DEFAULT_LAUNCHD_LABEL = 'com.cos.glasses-server'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* COS Control's LaunchAgent uses KeepAlive SuccessfulExit:false — a clean
|
|
9
|
+
* SIGTERM/exit(0) leaves the service STOPPED. Phone Restart must either
|
|
10
|
+
* kickstart -k (kill + start) or exit non-zero so KeepAlive brings it back.
|
|
11
|
+
*/
|
|
12
|
+
export function scheduleManagedServerRestart(options?: {
|
|
13
|
+
delayMs?: number
|
|
14
|
+
label?: string
|
|
15
|
+
kickstart?: (args: string[]) => void
|
|
16
|
+
exitProcess?: (code: number) => void
|
|
17
|
+
onError?: (error: unknown) => void
|
|
18
|
+
}): void {
|
|
19
|
+
const delayMs = options?.delayMs ?? 350
|
|
20
|
+
const label = (options?.label ?? process.env.COS_LAUNCHD_LABEL?.trim()) || DEFAULT_LAUNCHD_LABEL
|
|
21
|
+
const kickstart = options?.kickstart ?? ((args: string[]) => {
|
|
22
|
+
const child = spawn('launchctl', args, { detached: true, stdio: 'ignore' })
|
|
23
|
+
child.unref()
|
|
24
|
+
})
|
|
25
|
+
const exitProcess = options?.exitProcess ?? ((code: number) => { process.exit(code) })
|
|
26
|
+
const onError = options?.onError ?? ((error: unknown) => {
|
|
27
|
+
console.error('[recovery] Failed to schedule managed server restart:', error)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
if (process.env.COS_DISABLE_SELF_RESTART === '1') return
|
|
32
|
+
try {
|
|
33
|
+
const uid = userInfo().uid
|
|
34
|
+
kickstart(['kickstart', '-k', `gui/${uid}/${label}`])
|
|
35
|
+
// If kickstart did not kill us quickly (misconfigured label), force a
|
|
36
|
+
// non-zero exit so KeepAlive SuccessfulExit:false still restarts.
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
if (process.env.COS_DISABLE_SELF_RESTART === '1') return
|
|
39
|
+
try { exitProcess(1) } catch (error) { onError(error) }
|
|
40
|
+
}, 2_000).unref?.()
|
|
41
|
+
} catch (error) {
|
|
42
|
+
onError(error)
|
|
43
|
+
try { exitProcess(1) } catch (exitError) { onError(exitError) }
|
|
44
|
+
}
|
|
45
|
+
}, delayMs)
|
|
46
|
+
timer.unref?.()
|
|
47
|
+
}
|
|
@@ -21,9 +21,9 @@ export function managedRuntimeCapability(): ManagedRuntimeCapability {
|
|
|
21
21
|
// Whisper lifecycle is private to the local controller. It is never
|
|
22
22
|
// exposed as a network-reachable mutation capability.
|
|
23
23
|
restartWhisper: false,
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// Phone/companion may POST /api/recovery/server/restart when managed.
|
|
25
|
+
// launchd KeepAlive brings the process back after SIGTERM. Unmanaged
|
|
26
|
+
// foreground installs reject that route (restart_unmanaged).
|
|
27
27
|
restartServer: managed,
|
|
28
28
|
maintenanceDrain: managed,
|
|
29
29
|
lifecycleProof: managed,
|
|
@@ -2,6 +2,8 @@ import { Router } from 'express'
|
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import { atomicWriteFileSync } from '../lib/atomic-fs.js'
|
|
5
|
+
import { scheduleManagedServerRestart } from '../lib/managed-restart.js'
|
|
6
|
+
import { isManagedRuntime } from '../lib/managed-runtime.js'
|
|
5
7
|
import { acquireMaintenance, getRecoveryActivityStatus } from '../lib/recovery-activity.js'
|
|
6
8
|
import { getWhisperHealth, restartWhisperServer } from '../lib/whisper-local.js'
|
|
7
9
|
import { serverMetrics } from '../lib/server-metrics.js'
|
|
@@ -19,14 +21,16 @@ recoveryRouter.get('/live', (_req, res) => {
|
|
|
19
21
|
res.json({
|
|
20
22
|
status: 'ok', bootId: serverMetrics.bootId, pid: process.pid,
|
|
21
23
|
uptimeSeconds: Math.round((Date.now() - serverMetrics.startedAt) / 1000),
|
|
22
|
-
|
|
24
|
+
// COS Control LaunchAgent sets COS_MANAGED=1 with COS_HARNESS=foreground.
|
|
25
|
+
// Do not require COS_HARNESS=daemon — that false-negative blocked phone Restart.
|
|
26
|
+
managed: isManagedRuntime(),
|
|
23
27
|
})
|
|
24
28
|
})
|
|
25
29
|
|
|
26
30
|
recoveryRouter.get('/recovery/status', (_req, res) => {
|
|
27
31
|
res.json({
|
|
28
32
|
bootId: serverMetrics.bootId,
|
|
29
|
-
managed:
|
|
33
|
+
managed: isManagedRuntime(),
|
|
30
34
|
whisper: getWhisperHealth(),
|
|
31
35
|
asr: { hqActive: false, hqQueued: 0, fastRestarting: false }, // public build: no HQ/fast ASR scheduler in this server
|
|
32
36
|
activity: getRecoveryActivityStatus(),
|
|
@@ -46,7 +50,7 @@ recoveryRouter.post('/recovery/whisper/restart', async (_req, res) => {
|
|
|
46
50
|
})
|
|
47
51
|
|
|
48
52
|
recoveryRouter.post('/recovery/server/restart', (_req, res) => {
|
|
49
|
-
if (
|
|
53
|
+
if (!isManagedRuntime()) {
|
|
50
54
|
return res.status(409).json({ error: 'Server is not managed by the COS LaunchAgent', reason: 'restart_unmanaged' })
|
|
51
55
|
}
|
|
52
56
|
const elapsed = Date.now() - lastRestartAt()
|
|
@@ -62,15 +66,15 @@ recoveryRouter.post('/recovery/server/restart', (_req, res) => {
|
|
|
62
66
|
res.status(202).json({ accepted: true, oldBootId: serverMetrics.bootId })
|
|
63
67
|
// Schedule independently of the response socket. The phone may change
|
|
64
68
|
// network/close the sheet immediately after receiving 202; that must not
|
|
65
|
-
// cancel an accepted restart
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// cancel an accepted restart. Maintenance is in-process only — it dies with us.
|
|
70
|
+
//
|
|
71
|
+
// Do NOT SIGTERM→exit(0): Control's LaunchAgent KeepAlive is
|
|
72
|
+
// SuccessfulExit:false, so a clean exit leaves the service STOPPED and the
|
|
73
|
+
// phone shows "Restart requested; reconnect is taking longer than expected."
|
|
74
|
+
scheduleManagedServerRestart({
|
|
75
|
+
onError: (error) => {
|
|
71
76
|
gate.release()
|
|
72
|
-
console.error('[recovery] Failed to
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
timer.unref?.()
|
|
77
|
+
console.error('[recovery] Failed to schedule managed server restart:', error)
|
|
78
|
+
},
|
|
79
|
+
})
|
|
76
80
|
})
|