@gotcos/glasses-server 6.63.0 → 6.63.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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/server/index.ts +7 -1
- package/server/routes/welcome-context.ts +78 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 6.63.1 — 2026-10-06 — Weather survives a restart
|
|
2
|
+
|
|
3
|
+
- Home weather survives a server restart. The last good weather and place are kept in `<data>/welcome-weather.json`; when a forecast lookup fails (timeout, HTTP error, bad payload), the route answers with that saved weather if it is under 3 hours old, never older. A failed forecast or place lookup is now logged once per kind per 10 minutes, with the cause and whether a saved weather was served. Before, after a restart the first failed lookup sent the glasses no weather and nothing said why. Pairs with glasses 6.10.603, which also retries on Home and keeps its own last good weather.
|
|
4
|
+
- Security: `proxy-addr` 2.0.8 (through Express), for GHSA-jqcg-44mw-7w3h (IP spoofing through an IPv4-mapped IPv6 trust subnet). Lockfile only.
|
|
5
|
+
|
|
1
6
|
## 6.63.0 — 2026-10-05 — Background forks
|
|
2
7
|
|
|
3
8
|
- Background forks. `POST /api/agent-sessions/:provider/:threadId/fork` accepts an optional `clientForkId`: once every gate passes it answers 202 and runs the fork in the background, and `GET /api/agent-session-forks/:clientForkId` returns `running` or the exact answer the synchronous route would have sent. A repeated id replays its job and never makes a second copy. Outcomes are kept in `<data>/fork-jobs.json` (7 days, 200 rows); a fork still running when the server stops reads back as orphan-possible ("look in Sessions before forking again"), never as failed. Without the id the route is unchanged. Why: on 2026-10-05 COS Control waited 5 minutes for a 6.5-minute fork that succeeded and told Miles the server had stopped. Pairs with COS Control 0.5.257; older clients keep the synchronous route.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.63.
|
|
3
|
+
"version": "6.63.1",
|
|
4
4
|
"description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/index.ts
CHANGED
|
@@ -106,7 +106,7 @@ import { recoveryRouter } from './routes/recovery.js'
|
|
|
106
106
|
import { promptEditRouter } from './routes/prompt-edit.js'
|
|
107
107
|
import { lensGistRouter } from './routes/lens-gist.js'
|
|
108
108
|
import { bookmarksRouter } from './routes/bookmarks.js'
|
|
109
|
-
import { welcomeContextRouter } from './routes/welcome-context.js'
|
|
109
|
+
import { setWeatherPersistence, welcomeContextRouter } from './routes/welcome-context.js'
|
|
110
110
|
import { liveCuesRouter } from './routes/live-cues.js'
|
|
111
111
|
import { tasksRouter } from './routes/tasks.js'
|
|
112
112
|
import { memoryRouter } from './routes/memory.js'
|
|
@@ -143,6 +143,7 @@ import { createWorkIntakeRouter } from './routes/work-intake.js'
|
|
|
143
143
|
import { createWorkHandoffRequestsRouter } from './routes/work-handoff-requests.js'
|
|
144
144
|
import { createJevRouter } from './routes/jev.js'
|
|
145
145
|
import { dataPath } from './lib/data-dir.js'
|
|
146
|
+
import { loadJsonOrQuarantine, durableAtomicWriteFileSync } from './lib/atomic-fs.js'
|
|
146
147
|
import { WorkIntakeStore, createOptionalWorkIntakeStore } from './lib/work-intake-store.js'
|
|
147
148
|
import { WorkHandoffRequestStore, createOptionalWorkHandoffRequestStore, handoffRequestCapabilities } from './lib/work-handoff-requests.js'
|
|
148
149
|
import { createWorkBoardReader } from './lib/work-board-reader.js'
|
|
@@ -1065,6 +1066,11 @@ app.use('/api', recoveryRouter)
|
|
|
1065
1066
|
app.use('/api', promptEditRouter)
|
|
1066
1067
|
app.use('/api', lensGistRouter)
|
|
1067
1068
|
app.use('/api', bookmarksRouter)
|
|
1069
|
+
// 6.63.1: the last good weather and place survive a restart (<data>/welcome-weather.json).
|
|
1070
|
+
setWeatherPersistence({
|
|
1071
|
+
load: () => { const r = loadJsonOrQuarantine<unknown>(dataPath('welcome-weather.json')); return r.status === 'ok' ? r.data : null },
|
|
1072
|
+
save: (value) => durableAtomicWriteFileSync(dataPath('welcome-weather.json'), `${JSON.stringify(value)}\n`),
|
|
1073
|
+
})
|
|
1068
1074
|
app.use('/api', welcomeContextRouter)
|
|
1069
1075
|
app.use('/api', liveCuesRouter)
|
|
1070
1076
|
app.use('/api', tasksRouter)
|
|
@@ -25,6 +25,72 @@ let lastKnownCity = ''
|
|
|
25
25
|
let cachedNextEvent: NextEventPayload | null = null
|
|
26
26
|
let calendarFetchedAt = 0
|
|
27
27
|
|
|
28
|
+
// 6.63.1: after a server restart the first forecast lookup can fail and, with nothing cached, the
|
|
29
|
+
// glasses Home got no weather until the app was relaunched. The last good weather and
|
|
30
|
+
// place now survive a restart, and a failed lookup says why in the log (it was silent).
|
|
31
|
+
/** A saved weather older than this is never served after a restart. */
|
|
32
|
+
export const WEATHER_PERSIST_MAX_AGE_MS = 3 * 60 * 60_000
|
|
33
|
+
const WEATHER_FAILURE_LOG_MS = 10 * 60_000
|
|
34
|
+
|
|
35
|
+
export interface WeatherPersistence {
|
|
36
|
+
load(): unknown
|
|
37
|
+
save(value: unknown): void
|
|
38
|
+
}
|
|
39
|
+
let weatherPersistence: WeatherPersistence | null = null
|
|
40
|
+
let weatherRestored = false
|
|
41
|
+
const lastFailureLogAt = new Map<string, number>()
|
|
42
|
+
|
|
43
|
+
/** Wired by the server (index.ts, `<data>/welcome-weather.json`); off by default so tests touch nothing. */
|
|
44
|
+
export function setWeatherPersistence(p: WeatherPersistence | null): void {
|
|
45
|
+
weatherPersistence = p
|
|
46
|
+
weatherRestored = false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function restoreWeatherOnce(): void {
|
|
50
|
+
if (weatherRestored || !weatherPersistence) return
|
|
51
|
+
weatherRestored = true
|
|
52
|
+
try {
|
|
53
|
+
const v = weatherPersistence.load() as Record<string, unknown> | null
|
|
54
|
+
if (!v || typeof v !== 'object') return
|
|
55
|
+
const at = typeof v.fetchedAt === 'number' ? v.fetchedAt : 0
|
|
56
|
+
const w = v.weather as WeatherPayload | undefined
|
|
57
|
+
if (typeof v.lat === 'number' && typeof v.lon === 'number' && lastKnownLat == null) { lastKnownLat = v.lat; lastKnownLon = v.lon }
|
|
58
|
+
if (typeof v.city === 'string' && !lastKnownCity) lastKnownCity = v.city
|
|
59
|
+
const fresh = at > 0 && at <= Date.now() + 60_000 && Date.now() - at <= WEATHER_PERSIST_MAX_AGE_MS
|
|
60
|
+
if (fresh && w && typeof w.temp === 'string' && typeof w.desc === 'string' && !cachedWeather) {
|
|
61
|
+
cachedWeather = { temp: w.temp, desc: w.desc, location: typeof w.location === 'string' ? w.location : '' }
|
|
62
|
+
// Expired for the TTL check, so the next request still tries a fresh lookup; kept as the fallback.
|
|
63
|
+
weatherFetchedAt = 0
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn(`[welcome-context] saved weather unreadable: ${error instanceof Error ? error.message : error}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function persistWeather(): void {
|
|
71
|
+
if (!weatherPersistence) return
|
|
72
|
+
try {
|
|
73
|
+
weatherPersistence.save({ lat: lastKnownLat ?? null, lon: lastKnownLon ?? null, city: lastKnownCity, weather: cachedWeather, fetchedAt: weatherFetchedAt })
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn(`[welcome-context] saving weather failed: ${error instanceof Error ? error.message : error}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One line per failure kind per 10 minutes: the cause, never the coordinates. */
|
|
80
|
+
function noteWeatherFailure(kind: 'forecast' | 'geocode', reason: string): void {
|
|
81
|
+
const key = `${kind}:${reason}`
|
|
82
|
+
const now = Date.now()
|
|
83
|
+
if (now - (lastFailureLogAt.get(key) ?? 0) < WEATHER_FAILURE_LOG_MS) return
|
|
84
|
+
lastFailureLogAt.set(key, now)
|
|
85
|
+
console.warn(`[welcome-context] ${kind} lookup failed: ${reason}${cachedWeather ? ' (serving the last good weather)' : ' (no weather to serve)'}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function failureReason(error: unknown): string {
|
|
89
|
+
const name = error instanceof Error ? error.name : ''
|
|
90
|
+
if (name === 'TimeoutError' || name === 'AbortError') return 'timeout'
|
|
91
|
+
return error instanceof Error ? (error.message.slice(0, 80) || name || 'error') : 'error'
|
|
92
|
+
}
|
|
93
|
+
|
|
28
94
|
/** Full WMO 4677 — discrete codes, exact match required. */
|
|
29
95
|
export const WMO_CODES: Record<number, string> = {
|
|
30
96
|
0: 'Clear',
|
|
@@ -108,7 +174,7 @@ async function reverseGeocode(lat: number, lon: number): Promise<string> {
|
|
|
108
174
|
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`,
|
|
109
175
|
{ signal: AbortSignal.timeout(GEOCODE_TIMEOUT_MS) },
|
|
110
176
|
)
|
|
111
|
-
if (!res.ok) return lastKnownCity
|
|
177
|
+
if (!res.ok) { noteWeatherFailure('geocode', `http ${res.status}`); return lastKnownCity }
|
|
112
178
|
const data = await res.json() as {
|
|
113
179
|
city?: string
|
|
114
180
|
locality?: string
|
|
@@ -122,7 +188,8 @@ async function reverseGeocode(lat: number, lon: number): Promise<string> {
|
|
|
122
188
|
lastKnownCity = region ? `${city}, ${region}` : city
|
|
123
189
|
}
|
|
124
190
|
return lastKnownCity
|
|
125
|
-
} catch {
|
|
191
|
+
} catch (error) {
|
|
192
|
+
noteWeatherFailure('geocode', failureReason(error))
|
|
126
193
|
return lastKnownCity
|
|
127
194
|
}
|
|
128
195
|
}
|
|
@@ -144,6 +211,7 @@ export async function fetchWeather(
|
|
|
144
211
|
queryLat?: number,
|
|
145
212
|
queryLon?: number,
|
|
146
213
|
): Promise<WeatherPayload | null> {
|
|
214
|
+
restoreWeatherOnce()
|
|
147
215
|
const resolved = resolveWeatherCoords(queryLat, queryLon)
|
|
148
216
|
if (!resolved) return null
|
|
149
217
|
|
|
@@ -183,14 +251,14 @@ export async function fetchWeather(
|
|
|
183
251
|
`https://api.open-meteo.com/v1/forecast?latitude=${useLat}&longitude=${useLon}¤t=temperature_2m,weather_code&temperature_unit=fahrenheit&timezone=auto`,
|
|
184
252
|
{ signal: AbortSignal.timeout(FORECAST_TIMEOUT_MS) },
|
|
185
253
|
)
|
|
186
|
-
if (!res.ok) return cachedWeather
|
|
254
|
+
if (!res.ok) { noteWeatherFailure('forecast', `http ${res.status}`); return cachedWeather }
|
|
187
255
|
|
|
188
256
|
const data = await res.json() as {
|
|
189
257
|
current?: { temperature_2m?: number; weather_code?: number }
|
|
190
258
|
}
|
|
191
259
|
const temp = data.current?.temperature_2m
|
|
192
260
|
const code = data.current?.weather_code
|
|
193
|
-
if (typeof temp !== 'number' || typeof code !== 'number') return cachedWeather
|
|
261
|
+
if (typeof temp !== 'number' || typeof code !== 'number') { noteWeatherFailure('forecast', 'bad payload'); return cachedWeather }
|
|
194
262
|
|
|
195
263
|
await ensureCityLabel(useLat, useLon, source)
|
|
196
264
|
|
|
@@ -200,8 +268,10 @@ export async function fetchWeather(
|
|
|
200
268
|
location: lastKnownCity || `${useLat.toFixed(2)}, ${useLon.toFixed(2)}`,
|
|
201
269
|
}
|
|
202
270
|
weatherFetchedAt = Date.now()
|
|
271
|
+
persistWeather()
|
|
203
272
|
return cachedWeather
|
|
204
|
-
} catch {
|
|
273
|
+
} catch (error) {
|
|
274
|
+
noteWeatherFailure('forecast', failureReason(error))
|
|
205
275
|
return cachedWeather
|
|
206
276
|
}
|
|
207
277
|
}
|
|
@@ -272,6 +342,9 @@ export function _resetWelcomeContextCachesForTests(): void {
|
|
|
272
342
|
lastKnownCity = ''
|
|
273
343
|
cachedNextEvent = null
|
|
274
344
|
calendarFetchedAt = 0
|
|
345
|
+
weatherPersistence = null
|
|
346
|
+
weatherRestored = false
|
|
347
|
+
lastFailureLogAt.clear()
|
|
275
348
|
}
|
|
276
349
|
|
|
277
350
|
welcomeContextRouter.get('/welcome-context', async (req, res) => {
|