@gotcos/glasses-server 6.42.1 → 6.43.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 +103 -0
- package/README.md +30 -0
- package/package.json +1 -1
- package/server/index.ts +10 -0
- package/server/lib/message-reservations.ts +59 -0
- package/server/lib/morning-brief-config.ts +546 -0
- package/server/lib/morning-brief-coverage.ts +376 -0
- package/server/lib/morning-brief-prompt.ts +263 -0
- package/server/lib/morning-brief-runtime.ts +203 -0
- package/server/lib/morning-brief-schedule.ts +158 -0
- package/server/lib/morning-brief-scheduler.ts +412 -0
- package/server/lib/query-job-coordinator.ts +6 -0
- package/server/lib/query-job-runtime.ts +9 -0
- package/server/lib/query-job-store.ts +28 -0
- package/server/routes/health.ts +20 -0
- package/server/routes/message-ref.ts +6 -1
- package/server/routes/morning-brief.ts +94 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
+
## 6.43.1
|
|
2
|
+
|
|
3
|
+
Two things the first night of the morning brief taught us, plus the depth
|
|
4
|
+
behind each source.
|
|
5
|
+
|
|
6
|
+
- **A running brief no longer collides with your next message.** The brief
|
|
7
|
+
reserved #74 at 22:18 and was still running when the phone minted #74 for
|
|
8
|
+
the next prompt at 22:25, because `/api/message-counter` only counted
|
|
9
|
+
exchanges that had already been projected. Numbers held by admitted,
|
|
10
|
+
not-yet-terminal durable jobs (and by the brief's own ledger row, which
|
|
11
|
+
exists before the job does) now count toward the ceiling. New
|
|
12
|
+
`lib/message-reservations.ts` registry; the job store keeps the number on
|
|
13
|
+
its always-in-memory identity so the answer is synchronous and survives a
|
|
14
|
+
restart.
|
|
15
|
+
- **Sources show what is behind them.** `GET /api/morning-brief` now carries
|
|
16
|
+
`coverage`: one row per source with a state (`ready`, `empty`,
|
|
17
|
+
`unavailable`, `runtime`) and a summary line such as "2,312 meetings
|
|
18
|
+
stored, newest Sep 2026", "6,705 memories · 66 threads (12 active)", or
|
|
19
|
+
"/good-morning found under .claude/skills". Same wells COS Control's
|
|
20
|
+
Activity tiles read (meeting library, context status, the pipeline bridge
|
|
21
|
+
for calendar and tasks, the reflection log). Probes are cached for five
|
|
22
|
+
minutes and bounded to eight seconds each; a source the server cannot see
|
|
23
|
+
(Slack, health, dashboards) says so as `runtime` instead of showing a dash.
|
|
24
|
+
`GET /api/morning-brief/coverage?refresh=1` re-probes on demand.
|
|
25
|
+
- **Each run records what it was asked for and what came back.** The ledger
|
|
26
|
+
row stores the sections the brief was composed with, and once the job
|
|
27
|
+
completes the run view reports each as `present`, `unavailable` (the
|
|
28
|
+
prompt's "<Section>: unavailable" line), `skipped` (silent-skip sections
|
|
29
|
+
with nothing behind them), or `missing`. A skill section is present unless
|
|
30
|
+
the answer says the skill was not found.
|
|
31
|
+
|
|
32
|
+
No config migration: existing `config.json` and `runs.json` files load as
|
|
33
|
+
they are; older run rows simply have no section outcome.
|
|
34
|
+
|
|
35
|
+
## 6.43.0
|
|
36
|
+
|
|
37
|
+
The brief is waiting before you ask for it.
|
|
38
|
+
|
|
39
|
+
A start-of-day brief now runs on a schedule inside the server and lands in
|
|
40
|
+
the inbox as an ordinary numbered reply — no prompt, no "run the good-morning
|
|
41
|
+
skill", no phone awake at 07:00. Jun Kiat Lee's first-week note said it
|
|
42
|
+
plainly: he deployed COS to his G2 expecting a push at the start of the day and
|
|
43
|
+
nothing came, because until now the only thing that could start a brief was a
|
|
44
|
+
person typing. The server is the one process awake at that hour, so it owns
|
|
45
|
+
the schedule.
|
|
46
|
+
|
|
47
|
+
What you get, by default: weekdays at 07:00 in the Mac's own timezone, four
|
|
48
|
+
sections any COS brain can fill — Calendar, From recent meetings (decisions,
|
|
49
|
+
deadlines, owed items), Due (tasks inside a seven-day horizon), and Waiting on
|
|
50
|
+
you (unanswered mentions and asks across whatever channels the brain can
|
|
51
|
+
read). Everything else in the catalog — Knowledge graph, Reflection, Health,
|
|
52
|
+
an Opening reading, a Metrics pulse, a workspace skill, a custom section — is
|
|
53
|
+
off until you turn it on. The brief is composed from the sources you choose,
|
|
54
|
+
in the order you choose, with the windows you choose, so it is yours rather
|
|
55
|
+
than a template with your name on it.
|
|
56
|
+
|
|
57
|
+
How it works, so the cost is legible:
|
|
58
|
+
|
|
59
|
+
- One durable query job per local calendar day, submitted to the same
|
|
60
|
+
coordinator every phone prompt uses. It survives the phone being asleep and
|
|
61
|
+
is projected into the conversation store with a reserved message number when
|
|
62
|
+
it completes, so the companion's history hydration surfaces it like any
|
|
63
|
+
reply.
|
|
64
|
+
- The fire is remembered in a ledger written BEFORE admission. A crash between
|
|
65
|
+
that write and the 202 is resumed by a per-day client identity, never re-run.
|
|
66
|
+
A failed admission (store degraded, server draining) retries at most three
|
|
67
|
+
times, two minutes apart, then gives the day up.
|
|
68
|
+
- A Mac asleep through the slot still fires inside a catch-up window (three
|
|
69
|
+
hours by default, up to twelve). Past that, the day is skipped rather than
|
|
70
|
+
delivered at lunch.
|
|
71
|
+
- "Run now" is capped at five a day and refused while a brief is live.
|
|
72
|
+
- The whole thing is inert when COS Control's "Background jobs" switch is off
|
|
73
|
+
or maintenance admissions are closed. The prompt is read-only by contract:
|
|
74
|
+
it forbids sending, creating, or editing anything.
|
|
75
|
+
|
|
76
|
+
The prompt itself carries the evidence discipline the good-morning routine
|
|
77
|
+
learned the hard way: a source that cannot be read produces one honest
|
|
78
|
+
"unavailable" line; nothing is invented; only items with a hard edge (a
|
|
79
|
+
decision, a date, a dollar figure, an owner) make the cut; the scribe's
|
|
80
|
+
extracted action-item list is never pasted verbatim. Enable the "Workspace
|
|
81
|
+
skill" source with `/good-morning` and that skill's output IS the brief —
|
|
82
|
+
which is how Miles's own routine rides this without being rewritten.
|
|
83
|
+
|
|
84
|
+
Surface:
|
|
85
|
+
|
|
86
|
+
- `GET /api/morning-brief` — config, the source catalog (labels, descriptions,
|
|
87
|
+
option schemas a settings screen can render), status with `nextRunAt`, and
|
|
88
|
+
recent runs with live job status and the message number each produced.
|
|
89
|
+
- `PUT /api/morning-brief` — patch any field; a bad time, zone, model, or
|
|
90
|
+
skill name is a 400 with a named code, never a silent keep.
|
|
91
|
+
- `POST /api/morning-brief/run` — fire one now (202; 409 while one runs; 429
|
|
92
|
+
past the daily cap).
|
|
93
|
+
- `GET /api/morning-brief/preview` — the exact prompt today's brief would send.
|
|
94
|
+
- `/api/health` advertises `features.morningBrief` and
|
|
95
|
+
`capabilities.morningBrief` (`enabled`, `time`, `timezone`, `nextRunAt`,
|
|
96
|
+
`lastRunAt`, `lastRunStatus`, `gate`) — times and gate only, no prompt, no
|
|
97
|
+
ids.
|
|
98
|
+
|
|
99
|
+
Config lives at `~/.cos-glasses/data/morning-brief/config.json` (0600), the
|
|
100
|
+
ledger beside it. `COS_MORNING_BRIEF_DIR` relocates both. Schedule arithmetic
|
|
101
|
+
uses Node's Intl in the configured IANA zone, and is covered across both US DST
|
|
102
|
+
transitions and a zone east of UTC.
|
|
103
|
+
|
|
1
104
|
## 6.42.1
|
|
2
105
|
|
|
3
106
|
Hardening of the 6.42.0 display-stream ticket, from a four-validator QA pass
|
package/README.md
CHANGED
|
@@ -208,6 +208,36 @@ Telegram activity export is disabled by default even when a private COS
|
|
|
208
208
|
pipeline contains `.telegram_config.json`; enable it only with the explicit
|
|
209
209
|
`COS_TELEGRAM_NOTIFICATIONS=1` opt-in.
|
|
210
210
|
|
|
211
|
+
## Morning brief (6.43.0)
|
|
212
|
+
|
|
213
|
+
A start-of-day brief runs on a schedule inside the server and waits in the
|
|
214
|
+
inbox as a numbered reply. Default: weekdays at 07:00 in the Mac's timezone,
|
|
215
|
+
with Calendar, recent-meeting decisions, tasks due this week, and what is
|
|
216
|
+
waiting on you. Turn on more sources (knowledge graph, reflection, health, an
|
|
217
|
+
opening reading, a metrics pulse, one of your own skills such as
|
|
218
|
+
`/good-morning`, a custom section), reorder them, and set their windows from
|
|
219
|
+
COS Control or the companion, or directly:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
curl -H "X-COS-Token: $COS_TOKEN" http://127.0.0.1:3141/api/morning-brief
|
|
223
|
+
curl -H "X-COS-Token: $COS_TOKEN" -X PUT -H 'Content-Type: application/json' \
|
|
224
|
+
-d '{"time":"06:30","sources":[{"id":"skill","enabled":true,"options":{"name":"/good-morning"}}]}' \
|
|
225
|
+
http://127.0.0.1:3141/api/morning-brief
|
|
226
|
+
curl -H "X-COS-Token: $COS_TOKEN" -X POST http://127.0.0.1:3141/api/morning-brief/run
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
One provider run per local day, remembered in a ledger so a restart never
|
|
230
|
+
doubles it; a Mac asleep at the slot still fires inside a three-hour catch-up
|
|
231
|
+
window; "Run now" is capped at five a day. Off entirely when Background jobs
|
|
232
|
+
are off. The brief is read-only by contract.
|
|
233
|
+
|
|
234
|
+
Since 6.43.1 the same `GET /api/morning-brief` response carries `coverage`,
|
|
235
|
+
one row per source saying what the server can see behind it (meetings stored,
|
|
236
|
+
memories and threads, whether the named skill exists) and every run reports
|
|
237
|
+
which sections the answer actually opened. Numbers reserved by a running
|
|
238
|
+
brief count toward `/api/message-counter`, so the phone never mints the same
|
|
239
|
+
`#NNN` twice.
|
|
240
|
+
|
|
211
241
|
## Speaker diarization (opt-in)
|
|
212
242
|
|
|
213
243
|
Without a voiceprint model this server does not classify speakers at all — it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.43.1",
|
|
4
4
|
"description": "COS Glasses \u2014 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
|
@@ -83,6 +83,8 @@ import { initializeServerInstanceId } from './lib/server-instance-id.js'
|
|
|
83
83
|
import { appendPrivateEnvBlock, UnsafeUserConfigPathError } from './lib/secure-user-config.js'
|
|
84
84
|
import { getTranscriptionProfileStatus } from './lib/profile.js'
|
|
85
85
|
import { createQueryJobsRouter } from './routes/query-jobs.js'
|
|
86
|
+
import { createMorningBriefRouter } from './routes/morning-brief.js'
|
|
87
|
+
import { getMorningBriefScheduler, startMorningBriefScheduler, stopMorningBriefScheduler } from './lib/morning-brief-runtime.js'
|
|
86
88
|
import {
|
|
87
89
|
initQueryJobRuntime,
|
|
88
90
|
preparePublicDurableQueryAdmission,
|
|
@@ -488,6 +490,9 @@ app.use('/api', createQueryJobsRouter(queryJobCoordinator, {
|
|
|
488
490
|
prepareAdmission: preparePublicDurableQueryAdmission,
|
|
489
491
|
}))
|
|
490
492
|
app.use('/api', queryRouter)
|
|
493
|
+
// The scheduled start-of-day brief: settings, status, run-now. Same auth as
|
|
494
|
+
// every other settings route; the brief itself is an ordinary durable job.
|
|
495
|
+
app.use('/api', createMorningBriefRouter(getMorningBriefScheduler))
|
|
491
496
|
app.use('/api', providerProofRouter)
|
|
492
497
|
app.use('/api', transcribeRouter)
|
|
493
498
|
// Ported from cos-glasses-app in 6.24.0. The companion's Sessions tab has been
|
|
@@ -713,6 +718,7 @@ async function gracefulShutdown(): Promise<void> {
|
|
|
713
718
|
gracefulShutdownStarted = true
|
|
714
719
|
const forceExit = setTimeout(() => process.exit(1), 8_000)
|
|
715
720
|
forceExit.unref?.()
|
|
721
|
+
stopMorningBriefScheduler()
|
|
716
722
|
try {
|
|
717
723
|
await shutdownQueryJobRuntime('server_shutdown')
|
|
718
724
|
} catch (error) {
|
|
@@ -909,6 +915,10 @@ listenRequiredServers(listeners).then(() => {
|
|
|
909
915
|
} else {
|
|
910
916
|
console.log('[COS API] Durable query jobs: disabled by COS_DURABLE_QUERY_JOBS=0')
|
|
911
917
|
}
|
|
918
|
+
// The morning brief rides the durable coordinator, so it starts only once
|
|
919
|
+
// that store is ready. Its own tick checks the durable-jobs switch and the
|
|
920
|
+
// maintenance gate, so starting it here is safe when either is off.
|
|
921
|
+
startMorningBriefScheduler()
|
|
912
922
|
}).catch(error => {
|
|
913
923
|
// The store remains degraded and rejects admission. Legacy /api/query is
|
|
914
924
|
// still mounted, so the kill switch is an immediate rollback.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Message-number reservations that are not exchanges yet.
|
|
2
|
+
//
|
|
3
|
+
// The phone mints its own #NNN from the `max` that /api/message-counter
|
|
4
|
+
// reports, and a durable job carries the number it was minted with until its
|
|
5
|
+
// terminal projection writes the exchange into the session. Between admission
|
|
6
|
+
// and projection the number existed nowhere the counter looked, so a second
|
|
7
|
+
// producer minted it again: on 2026-09-01 the scheduled morning brief reserved
|
|
8
|
+
// #74 at 22:18:40 and the phone handed #74 to "Cross surface" at 22:25:15,
|
|
9
|
+
// because its counter only re-syncs at boot and the server said max was 73.
|
|
10
|
+
//
|
|
11
|
+
// Every holder of a not-yet-projected number registers a source here. The
|
|
12
|
+
// counter route and the brief's own reservation read the union. Sources
|
|
13
|
+
// register themselves rather than being imported, because the morning-brief
|
|
14
|
+
// runtime already imports the counter route (a direct import would cycle).
|
|
15
|
+
|
|
16
|
+
import { exchangeBelongsToEra } from './message-era.js'
|
|
17
|
+
|
|
18
|
+
export interface MessageReservation {
|
|
19
|
+
globalMsgNum: number
|
|
20
|
+
messageEra?: string
|
|
21
|
+
/** Diagnostic owner label, e.g. `job:<id>` or `brief:<day>`. Never a prompt. */
|
|
22
|
+
owner: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MessageReservationSource = () => readonly MessageReservation[]
|
|
26
|
+
|
|
27
|
+
const sources = new Set<MessageReservationSource>()
|
|
28
|
+
|
|
29
|
+
export function registerMessageReservationSource(source: MessageReservationSource): () => void {
|
|
30
|
+
sources.add(source)
|
|
31
|
+
return () => { sources.delete(source) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every live reservation in `era`. A source that throws contributes nothing;
|
|
35
|
+
* the counter must never fail because one holder is mid-shutdown. */
|
|
36
|
+
export function reservedGlobalMsgNums(era: string): MessageReservation[] {
|
|
37
|
+
const out: MessageReservation[] = []
|
|
38
|
+
for (const source of sources) {
|
|
39
|
+
let items: readonly MessageReservation[]
|
|
40
|
+
try { items = source() } catch { continue }
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (!Number.isSafeInteger(item.globalMsgNum) || item.globalMsgNum <= 0) continue
|
|
43
|
+
if (!exchangeBelongsToEra(item, era)) continue
|
|
44
|
+
out.push(item)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function maxReservedGlobalMsgNum(era: string): number {
|
|
51
|
+
let max = 0
|
|
52
|
+
for (const item of reservedGlobalMsgNums(era)) if (item.globalMsgNum > max) max = item.globalMsgNum
|
|
53
|
+
return max
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Test-only. */
|
|
57
|
+
export function resetMessageReservationSourcesForTests(): void {
|
|
58
|
+
sources.clear()
|
|
59
|
+
}
|