@gotcos/glasses-server 6.98.1 → 6.99.12
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 +149 -0
- package/package.json +2 -1
- package/server/index.ts +147 -9
- package/server/lib/api-auth.ts +31 -14
- package/server/lib/claude-bridge.ts +21 -2
- package/server/lib/codex-bridge.ts +21 -2
- package/server/lib/display-bus.ts +3 -0
- package/server/lib/drop-destinations.ts +134 -0
- package/server/lib/drop-history-search.ts +97 -0
- package/server/lib/drop-p2-boot.ts +60 -0
- package/server/lib/drop-p2-runtime.ts +237 -0
- package/server/lib/drop-p2-settings.ts +174 -0
- package/server/lib/drop-resolution.ts +168 -0
- package/server/lib/drop-router.ts +325 -0
- package/server/lib/drop-service.ts +739 -0
- package/server/lib/drop-settings.ts +45 -0
- package/server/lib/drop-store.ts +428 -0
- package/server/lib/drop-when.ts +265 -0
- package/server/lib/glasses-pairing.ts +15 -0
- package/server/lib/learning-needs-you.ts +111 -0
- package/server/lib/live-cues-v2-settings.ts +2 -1
- package/server/lib/live-cues-v2.ts +6 -4
- package/server/lib/meeting-consolidation-run.ts +169 -0
- package/server/lib/meeting-store.ts +61 -0
- package/server/lib/meeting-title-steer.ts +200 -0
- package/server/lib/network-policy.ts +84 -6
- package/server/lib/permission-broker.ts +11 -0
- package/server/lib/ping-budget.ts +149 -0
- package/server/lib/ping-engine.ts +245 -0
- package/server/lib/ping-feed-sink.ts +33 -0
- package/server/lib/ping-sources.ts +192 -0
- package/server/lib/query-job-coordinator.ts +5 -2
- package/server/lib/query-job-runtime.ts +45 -4
- package/server/lib/query-job-types.ts +10 -1
- package/server/lib/reminder-scheduler.ts +153 -0
- package/server/lib/tailnet-self-name.ts +103 -0
- package/server/lib/tailscale-nudge.ts +40 -2
- package/server/lib/task-store.ts +32 -0
- package/server/lib/web-ask-admission.ts +56 -0
- package/server/lib/web-live-chunks.ts +273 -0
- package/server/lib/web-memories-bridge.ts +323 -0
- package/server/lib/web-session-auth.ts +492 -0
- package/server/lib/web-usage-bridge.ts +154 -0
- package/server/lib/work-search.ts +14 -1
- package/server/lib/your-move-count.ts +58 -0
- package/server/routes/display.ts +11 -1
- package/server/routes/drops.ts +260 -0
- package/server/routes/health.ts +16 -0
- package/server/routes/live-cues.ts +5 -4
- package/server/routes/meeting.ts +61 -5
- package/server/routes/permission-broker.ts +23 -3
- package/server/routes/transcribe-stream.ts +34 -3
- package/server/routes/web-control.ts +569 -0
- package/server/routes/welcome-context.ts +2 -1
- package/server/web-control/app.css +519 -0
- package/server/web-control/app.js +3643 -0
- package/server/web-control/control.css +312 -0
- package/server/web-control/fonts/DMSans.ttf +0 -0
- package/server/web-control/fonts/Fraunces.ttf +0 -0
- package/server/web-control/fonts/JetBrainsMono.ttf +0 -0
- package/server/web-control/index.html +275 -0
- package/server/web-control/mark.svg +1 -0
- package/server/web-control/memories/d3.min.js +2 -0
- package/server/web-control/memories/graph-explorer.css +200 -0
- package/server/web-control/memories/graph-explorer.js +748 -0
- package/server/web-control/memories/memories-app.js +1561 -0
- package/server/web-control/memories/memories-theme.css +73 -0
- package/server/web-control/memories/memories.html +72 -0
- package/server/web-control/memories/memory-stewardship.js +63 -0
- package/server/web-control/memories/memory-workspace.js +216 -0
- package/server/web-control/panes/cos-chat.css +157 -0
- package/server/web-control/panes/cos-chat.js +973 -0
- package/server/web-control/panes/dictation.js +173 -0
- package/server/web-control/panes/meetings.css +257 -0
- package/server/web-control/panes/meetings.js +2264 -0
- package/server/web-control/panes/messages.js +711 -0
- package/server/web-control/panes/questions.css +32 -0
- package/server/web-control/panes/questions.js +275 -0
- package/server/web-control/panes/sessions.css +452 -0
- package/server/web-control/panes/settings.js +292 -0
- package/server/web-control/panes/speakers.css +269 -0
- package/server/web-control/panes/speakers.js +2755 -0
- package/server/web-control/panes/threads.js +200 -0
- package/server/web-control/panes/work.css +261 -0
- package/server/web-control/panes/work.js +1667 -0
- package/server/web-control/shell.js +294 -0
- package/server/web-control/theme.js +25 -0
- package/server/web-control/vendor/qrcode-encoder.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,152 @@
|
|
|
1
|
+
## 6.99.12 - COS Control Web catches up with the Even app: Ask with files, Reply, session questions, wide Usage (private)
|
|
2
|
+
|
|
3
|
+
2026-11-05. Private build for one Mac, on top of 6.99.11 (Drop it P2), which carries 6.99.10 (Drop it P1 and the web Ask sheet fix). Pairs with COS Control 0.5.371 or 0.5.372. Nothing changes for the glasses app.
|
|
4
|
+
|
|
5
|
+
Miles, 11/04 23:38: "On click it shows that it sent but it never shows up in messages. Also there isn't the ability to send a message from the web view." The four gaps the parity audit put first (`PARITY_even_app_vs_control_web_2026-11-04.md`), closed:
|
|
6
|
+
|
|
7
|
+
- **Ask COS in Messages, with or without Drop it.** Wherever durable query jobs are on (the default), Messages pins an Ask COS row first in Recent and opens a chat with a composer (`#/messages/cos`; beside the list from 860 px). Answers stream with their message number and land in Recent as numbered turns, here, on the Mac and on the lens. With Drop it on, the chat is the pinned COS chat of 6.99.10 with its Drop button; with it off, there is one Ask button and no inbox read. Return asks; Option-Return drops only where Drop shows; the buttons need no key on the phone. The Model menu now comes from `/api/models` (Claude Opus, Fable and Sonnet, Codex from the Mac's catalog, Cursor and Ollama when the Mac says they are ready), with an Effort menu (High, Extra high, Max, Ultracode) sent as `effort`.
|
|
8
|
+
- **The Ask sheet's Ask COS now shows its answer.** It hands the words to the same composer path and opens the chat, so the answer streams on screen instead of a status line that vanished.
|
|
9
|
+
- **Attachments on Ask, as in the Even app.** The clip opens Camera, Photo Library or Files on iOS (`<input type=file multiple>` for photos, PDF, TXT, MD, CSV, JSON, XLSX, MP4 and MOV); paste and drag-drop work too. Photos are drawn into a canvas at most 1024 px on the longest edge, JPEG quality 0.85, and posted to `POST /api/media` (`kind: user_photo`); documents and video go raw to `POST /api/media/file` with `X-COS-Filename`, `X-COS-Captured-At` and `X-COS-Web: 1`. The Ask sends up to five `attachmentIds`. Chips show a thumbnail, the name, the size, upload progress and Remove. The server caps are said plainly before anything uploads (5 per message, 2 MB per photo after resizing, 64 MB per document, 100 MB per video). HEIC is refused outside Safari with a line saying why. A 401 during an upload never opens the sign-in screen: the words and files stay, the chip says signed out, and Sign in again then Retry sends them. Drop takes words only; trying to drop with files attached says so. Uploads use XMLHttpRequest for progress (fetch has none); same origin, same cookie, same headers.
|
|
10
|
+
- **Reply from the message reader.** The reader ends with the same composer, sending that turn's `sessionId`, so the answer continues that conversation while it is live (else the server starts a new one, as before). The reader is not rebuilt under a half-written reply when Recent refreshes.
|
|
11
|
+
- **Session questions on the web.** `GET /api/session-questions` takes the COS Control Web session as well as `X-Cos-Token`, through the same cookie check the `/api` gate runs; the hook token still opens nothing. A browser poll counts as a live answering client only with `?client=web` (the page sends it only while a visible, signed-in tab shows the cards), in the phone's 30 s window, and never reports presence, so it never enables the away hold. A cookie poll naming `glasses` or `phone`, or no client, never counts; a token poll naming `web` never counts. `/api/models` `capabilities.sessionQuestions` gains `webClient: true`; the page never calls the route without it. Question and approval cards show over every pane, with the Even app's actions: pick (or Other), Send, Allow once, Deny (each tap then confirm), and Leave for the Mac, through the existing `POST .../answer`. A 401 on the poll stops it quietly. Plan approvals never reach the broker (ExitPlanMode is unbrokered), so there is no plan card.
|
|
12
|
+
- **Usage on the web uses the Mac's layout from 860 px wide, and custom date ranges** (merged from `claude/web-usage-wide` 5dfebf0). Below 860 px it is byte for byte the 6.99.10 page.
|
|
13
|
+
- Canary 12: with a message era active, a web Ask with a photo and a PDF through the real `/api/media`, `/api/media/file` and `/api/query-jobs` is admitted, the model receives both, the answer turn carries both refs, the counter moves by exactly one, and a Reply continues the same session.
|
|
14
|
+
- Default-on changes, for every web user:
|
|
15
|
+
- Messages shows the pinned Ask COS row and the two-column layout from 860 px wherever durable query jobs are on (the default); before, only with Drop it on.
|
|
16
|
+
- The message reader shows Reply.
|
|
17
|
+
- The Ask sheet's Ask COS opens the chat and streams the answer.
|
|
18
|
+
- Session question and approval cards show on the web wherever the permission broker is on (the default), and a visible Control Web tab now counts as a client that can answer, so the Mac holds a question for it (up to the broker's deadline) instead of showing its own dialog at once.
|
|
19
|
+
- Usage uses the Mac's layout from 860 px wide.
|
|
20
|
+
- `capabilities.sessionQuestions.webClient` on `/api/models` for every install (additive).
|
|
21
|
+
|
|
22
|
+
## 6.99.11 - Drop it P2: pings, follow-ups, reminders and History search (private)
|
|
23
|
+
|
|
24
|
+
2026-11-04. Private build for one Mac, on top of 6.99.10 (Drop it P1, with the web Ask sheet fix), which carries 6.99.9, 6.99.8 (resumable meeting consolidation) and the 6.99.7 dictation hotfix. Pairs with COS Control 0.5.371 now; COS Control 0.5.372 adds the Mac banners and the local reminder backup. Nothing changes for the glasses app yet.
|
|
25
|
+
|
|
26
|
+
Phase 2 of Drop it, the part that reaches out. Everything here runs only while Drop it is on (off for every user; on where `COS_DROP_IT=1` is set or Control turns it on), checked at boot and each minute. While off, nothing new opens and nothing is written.
|
|
27
|
+
|
|
28
|
+
- **Reminders.** "remind me Thursday to send Gina the JCK numbers" now reads its time: today, tonight, tomorrow, weekdays, next week, "at 3", "3:30 p.m.", "15:00", noon, morning, afternoon, evening, "in 20 minutes", "in 2 weeks", "Nov 12" and ISO dates. A day alone means 9:00, today's weekday means next week, and a bare hour means the next one. A time already past is refused, never moved. The receipt says "Reminder set: tomorrow 9:00 AM". With no time it asks once (Tomorrow 9:00, Monday 9:00) and waits on you until you pick (`when:` on `POST /api/inbox/action`).
|
|
29
|
+
- **Reminders fire.** A 30-second tick. After sleep the first tick fires what is overdue, once, and says how late past 10 minutes. `firedAt` is written before the notification, so nothing fires twice. Done, Snooze 1 hour and Tomorrow 9 on the ping. A reminder he set fires at any hour. `GET /api/drops/reminders` lists the pending ones for COS Control's local backup, which uses the same notification id (`reminder-<id>`), so the two never double.
|
|
30
|
+
- **Pings.** COS posts to the pinned chat, reading signals that already exist: your next meeting 15 minutes before it starts, follow-ups ready from a meeting, a thread gone quiet (at most once a week each), a red signal, an agent finishing work you sent it, and a Monday 9:00 check-in. Fixed wording over each source's own titles; no model is called, and a test fails if the ping code can reach any AI, mail, Slack or Telegram module. Work pings that arrive after 6 PM, before 6 AM or on a weekend wait and post as one "While you were off" item at 8:00, minus anything that moved on.
|
|
31
|
+
- **Follow-ups.** One line under the drop when what it became resolves: a card reaching QA or Complete, or archived; a handoff completed (with its report), failed or refused; a session you sent it to replied or stopped; a thread resolved. Exactly once per change, across restarts and a QA, Draft, QA bounce. Found by a 2-minute poll, sooner after a card write through this server, and at once when a session's turn ends.
|
|
32
|
+
- **Banners.** At most 6 a weekday, 2 per source, 45 minutes apart, none in quiet hours or before 8:00. Reminders are exempt. On by default only for reminders and follow-ups to your own drops; the rest land silently in the chat. `GET /api/inbox/banners` is the outbox COS Control reads. A reminder also goes to the lens as display event `cos_ping` (withheld from any stream without a ticket; shipped glasses builds ignore it).
|
|
33
|
+
- **Waiting on you.** Pings and follow-ups that need an answer (a card at QA, a failed or refused handoff, follow-ups ready, the check-in, a session that stopped, a reminder with no time) count toward `waiting` until answered, done or dismissed.
|
|
34
|
+
- **History search by meaning.** `POST /api/drops/search`: one Jev Choice over your drops' words and where they went, charged to the drop budget (`COS_DROP_DAILY_TOKENS`) and off with `COS_DROP_ROUTER=0`. A drop that looks like it holds a password or key is never offered. Over 254 drops it searches the newest 254 and says so; word search still covers everything. Read-only, so it is not held during an update drain.
|
|
35
|
+
- **COS Control Web.** The pinned chat's new buttons work: Done, Snooze, Tomorrow 9, the reminder time picks, Open result, Open, Review, Prep me, and Reply on the check-in.
|
|
36
|
+
- **Settings: tune and steer it.** `GET`/`PUT /api/drops/settings/p2` (its own file, `data/drop-p2-settings.json`; the P1 switch and its route are unchanged), and a COS reaching out section in COS Control Web Settings. Every value is honored by the code that does the thing:
|
|
37
|
+
- Pings: on or off; interruptions a day (6), from one kind a day (2), minutes between (45); quiet from and until (6 PM to 6 AM) and weekends; the first interruption (8:00); a switch for meeting prep, agents finishing, follow-ups ready, threads gone quiet and red signals.
|
|
38
|
+
- Follow-ups on your drops: on or off, and Work cards, handoffs, sessions and threads one by one.
|
|
39
|
+
- Weekly check-in: on or off, its day and time (Monday 9:00).
|
|
40
|
+
- Reminders: the default time (what a day alone means, and Tomorrow and Monday), the snooze length (the button says it), notifications, and the glasses push.
|
|
41
|
+
- Which kinds may interrupt; the rest land in the chat without a notification.
|
|
42
|
+
- Steer: Ping me about (a ping that mentions one of these words may interrupt, even when its kind stays in the chat; the limits still hold) and Leave alone (a ping that mentions one is not posted; reminders and follow-ups always are). Whole words, 300 characters each, no model.
|
|
43
|
+
- Numbers are clamped and a save changes only what it names. Readable and writable while Drop it is off. `capabilities.dropIt.p2Settings` lets older clients hide the controls.
|
|
44
|
+
- **Quiet during updates.** Every P2 loop skips its turn while the server drains; a reminder then fires on the next tick, marked late.
|
|
45
|
+
- **State.** `data/pings/state.json` (the banner ledger, held items, consumed keys): one writer, a 0700 folder and a 0600 file. Logs carry counts only, never words.
|
|
46
|
+
- Default-on changes: none for anyone with Drop it off, which is every install by default. With Drop it on: reminder times, reminder and follow-up banners, the Monday check-in, and silent pings in the chat, all adjustable in Settings. `capabilities.dropIt` gains `p2Settings: 1` for every install (additive).
|
|
47
|
+
|
|
48
|
+
## 6.99.10 - Drop it P1, with Ask and Drop on the web (private)
|
|
49
|
+
|
|
50
|
+
2026-11-04. Private build for one Mac, on top of 6.99.9 (web phone link and provider logos), which carries 6.99.8 (resumable meeting consolidation) and the 6.99.7 dictation hotfix. Pairs with COS Control 0.5.371. Nothing changes for the glasses app.
|
|
51
|
+
|
|
52
|
+
Server half of Drop it phase 1: a pinned COS chat that takes a drop (a thought, a task, a reminder, a question), files it, and answers with a receipt. Off for every user; on only where `COS_DROP_IT=1` is set or Control writes `PUT /api/drops/settings {"enabled":true}`. While off, every new route except the settings pair answers 404 `drop_it_off` and no drop or chat file is created. Messages, the lens message counter, `all-messages` and `live-chats` are unchanged either way.
|
|
53
|
+
|
|
54
|
+
- **Drops and the chat.** Two new stores beside the Messages store, never inside it: `data/drops/drops.json` (each drop, where it went, a status timeline) and `data/inbox/feed.json` (the chat: drop, receipt, question, answer, follow-up). One writer lock each, a 0700 folder and 0600 files, atomic writes, and a bad row set aside on its own. A drop is saved before it is filed, so a restart mid-filing keeps it as a note.
|
|
55
|
+
- **Where a drop goes.** "remind me" is a reminder, "remember" a memory, and a question goes to the existing Ask job, all by rule with no Jev call. Anything else is one Jev request: it files on its own at 0.80 with a 0.25 lead (a memory needs 0.90), otherwise it asks one question with the top two readings and Just a note. When Jev cannot run, the drop is kept as a plain note and the receipt says why.
|
|
56
|
+
- **Its own budget, Jev only.** Cap `COS_DROP_DAILY_TOKENS` (300,000 a day), its own ledger (`jev-drop-usage.json`) and breaker, and `COS_DROP_ROUTER=0` turns Jev routing off. Token audit caller `drop`, sizes only. Pinned to Jev for P1: the decision provider setting (SubQ) does not apply, and there is no `drop` decision use case, so the settings file stays readable by 6.99.4 and earlier. No claude -p and no new model caller.
|
|
57
|
+
- **Privacy.** A drop that looks like it holds a password or key is checked before any rule and kept as a plain note: it never reaches memory, the Ask job or Jev, and Move to a memory is refused. Jev reads cleaned words only. Where words do leave the Mac: Jev (api.typesafe.ai) for routing, the existing Ask job for a question, and, for a memory, the COS bridge embeds the words through the embedding provider it uses for every memory (OpenAI today). Logs carry ids, codes and lengths, never words.
|
|
58
|
+
- **Filing writes.** New card through the bridge verb `task-capture-drop` (answers the card id; an open card with the same words is linked, never copied), memory through `memory-capture` into To review (a new capture key per filing), thread notes through `thread-source`/`thread-unsource` by id. A bridge without a verb keeps a plain note and says so; a memory too short for the memory contract is kept as a note and says that. Existing card, session follow-up (held until Send), note and reminder live in the drop store only. Reminder times and firing come in P2.
|
|
59
|
+
- **Undo and Move.** 24 hours, refused with a reason when the card or thread note was edited, or the memory reviewed, since (`memory-uncapture`). Undo never archives or removes something the drop did not create. Move is Undo, then filing where you picked, with no Jev call, and a target is checked before anything is undone. Nothing is deleted.
|
|
60
|
+
- **Retention.** Drops a year; an undone drop 30 days; a drop's chat lines go when the drop goes. Neither store ever writes a file larger than it will read back (32 MB).
|
|
61
|
+
- **Routes.** `POST/GET /api/drops`, `GET /api/drops/:id`, `POST /api/drops/:id/{undo,move,ask,sent}`, `GET /api/inbox/summary`, `GET /api/inbox/feed`, `POST /api/inbox/{read,action}`, `GET/PUT /api/drops/settings`. History is word search with kind and status filters.
|
|
62
|
+
- **Capability.** `capabilities.dropIt` `{ protocol: 1, enabled, source }` on `/api/health` and `/api/models`. Additive.
|
|
63
|
+
- **Waiting on you.** `GET /api/inbox/summary` adds `waiting`, a whole number: questions COS asked that you have not answered, chat items marked `needsReply` (a follow-up or ping that needs a decision), and the Work items in Your move that want an answer or a look (needs input, attention, done; a run still going does not count). Unread clears when the chat is opened; waiting clears only when the item is answered, done or dismissed (`POST /api/inbox/action {action: "dismiss"}`, for a `needsReply` item). `POST /api/inbox/read` answers `waiting` too. The Your move count shares Work's own board read and is kept 30 seconds, and the summary waits at most 1.5 seconds for it. COS Control 0.5.371's Messages badge reads this field.
|
|
64
|
+
- **Ask from COS Control Web.** `POST /api/query-jobs` with `source: "web"` (the glasses' own route, behind the web sign-in and its write check): the server picks the conversation (the one the web is in today, or a new one) and gives the turn the next message number, from the same count `/api/message-counter` serves. The answer is a normal Messages turn: it shows in Recent on the Mac and the web, and in the glasses' Messages list. A retry of the same request reuses its number. Any other caller is unchanged.
|
|
65
|
+
- **COS Control Web, while Drop it is on.** Messages pins a COS row first in Recent (`#/messages/cos` opens the chat), and from 860 px the list and the chat sit side by side. The chat's composer has two visible buttons, Ask (filled) and Drop (outlined), so a phone with no keyboard needs no key combination or long press; with a keyboard, Return asks and Option-Return drops. Ask streams its answer with its message number, with Steer (where the Mac allows it) and Cancel; the Model menu lists what meeting Ask offers. Drop shows the receipt with Undo and Move. The rail and the section menu show a number on Messages for what waits on you, and a dot when there is only something unread; both are read with the page's existing status check, with no new timer, and never redraw the pane. With Drop it off the web looks exactly as in 6.99.9.
|
|
66
|
+
- **Fix: Ask COS in the web's Ask sheet did nothing.** Tapping it sent an older request (a non-standard id, no message era), so a Mac with a message era refused every one and only a small status line said so. It now sends the same request as the COS chat's Ask, and a refusal also shows a toast.
|
|
67
|
+
- Default-on changes: none. Drop it stays off until it is turned on. The `waiting` field and the web Ask route change nothing until a client uses them.
|
|
68
|
+
|
|
69
|
+
## 6.99.9 - Open COS on the web from the glasses app, and real provider logos (private)
|
|
70
|
+
|
|
71
|
+
2026-11-04. Private build for one Mac, on top of 6.99.8 (meeting consolidation, with the 6.99.7 dictation hotfix). Pairs with COS Glasses 6.11.672. Nothing changes for the COS Control Mac app.
|
|
72
|
+
|
|
73
|
+
- **A sign-in link for your phone's browser.** COS Glasses 6.11.672 adds Open COS on the web in Settings. It asks the Mac for a link, copies it, and you paste it into Safari or any browser to land on COS Activity already signed in, over Tailscale. The link works once and lasts 2 minutes; a short sign-in code is shown with it as a fallback. Links from the Even app open inside Even's own browser, never Safari, so copying is the way out (tested on an iPhone).
|
|
74
|
+
- New route `POST /api/web-session/phone-link`, for the paired glasses app only (its token; a signed-in browser cookie is refused). Callers must be on this Mac, Tailscale or the home network. At most 5 links a minute. The link and the code are never written to the log. `/api/health` advertises `capabilities.webPhoneLink`, so older glasses builds and older servers simply do not show the row.
|
|
75
|
+
- An expired link now says to get a new one from COS Control or the COS Glasses app.
|
|
76
|
+
- **Sessions on the web show the real Claude, OpenAI and Cursor logos**, the same marks COS Control draws, instead of plain shapes.
|
|
77
|
+
- Default-on changes: the web Sessions logos. The route answers only the paired glasses app.
|
|
78
|
+
|
|
79
|
+
## 6.99.8 - Resumable meeting consolidation (private)
|
|
80
|
+
|
|
81
|
+
- Explicit meeting cleanup jobs continue across bounded 15-minute provider passes under one durable job, model selection and cancellation control. Ordinary chats retain their current limits.
|
|
82
|
+
- Each pass closes its provider process tree before continuation. Partial output at the pass limit is progress, never a completed cleanup.
|
|
83
|
+
- A saved inventory, verified evidence hashes and final audit report gate completion. Provider failures, cancellation, three unchanged checkpoints or the 12-hour/48-pass safeguard stop honestly with saved work intact.
|
|
84
|
+
- Exposes the maintenance workflow capability for compatible COS Control clients. Interrupted server boots retain the existing interruption/orphan recovery policy; this does not silently restart writes after a server crash.
|
|
85
|
+
|
|
86
|
+
## 6.99.7 - Dictation no longer fails while meetings record (private hotfix)
|
|
87
|
+
|
|
88
|
+
2026-11-04. Private hotfix for one Mac, on top of 6.99.5. 6.99.6 is a separate meeting consolidation build, not yet installed; it needs this fix merged before it ships. Nothing changes for the glasses, the phone companion or the COS Control Mac app.
|
|
89
|
+
|
|
90
|
+
- **Fix: Mac dictation said "Server stopped" while a meeting was recording.** Asking whether a meeting was saved read and parsed every saved meeting's chunk file (813 files, 1.8 GB on the test Mac) whenever the meeting was not saved yet, which is every live recording. COS Control asks that of each live recording every few seconds, so two recordings kept the server busy for 4 to 17 seconds at a time. Dictation's 5 second check timed out, and live chunks took up to 26 seconds. Now each saved file's session is remembered until the file changes, and its first 4 KB is enough for most files. On the test Mac one lookup went from about 4.7 seconds to 5 ms (575 ms the first time after a start). Results were identical for 37 saved meetings, 6 of them Mac recordings.
|
|
91
|
+
- Default-on changes: none. The lookup gives the same answers, only faster.
|
|
92
|
+
|
|
93
|
+
## 6.99.5 - Control Web: questions asked on the Mac show on the phone (private)
|
|
94
|
+
|
|
95
|
+
2026-11-04. Private build for one Mac, on top of 6.99.4. Fixes from the web and Mac meeting comparison. Nothing changes for the glasses, the phone companion or the COS Control Mac app.
|
|
96
|
+
|
|
97
|
+
- **Meeting Ask stays in step.** A question asked on the Mac now appears on the phone while the live meeting is open. The page re-reads the Ask history every 12 seconds, and at once when you come back to the page. The panel redraws only when something changed.
|
|
98
|
+
- **Files.** A meeting with files shows the Mac's "Files · N" row, in the live view and on the saved meeting. Open it for the names and any that could not be read. Files are still added and removed in COS Control on your Mac.
|
|
99
|
+
- **The meeting's name.** A name typed in COS Control while recording now shows on the web. Without one, the web uses the calendar event that started within 20 minutes of the recording, as the Mac does.
|
|
100
|
+
- **Opening a page right after signing in is fast again.** The side rail's story lines load two at a time instead of all at once, so the page you open is never stuck behind them. A meeting opened 4 seconds after sign-in waited 10 to 14 seconds for its first read; it now takes under 2.
|
|
101
|
+
- Default-on changes: the web's background Ask history read (one GET every 12 seconds while a live meeting's panel is open). No change to the Mac app or the glasses.
|
|
102
|
+
|
|
103
|
+
## 6.99.4 - Control Web matches the Mac (private)
|
|
104
|
+
|
|
105
|
+
2026-11-04. Private build for one Mac, on top of 6.99.3. COS Control Web, the page at `/control`, now looks and behaves like the Mac app's panes. Nothing changes for the glasses, the phone companion or the COS Control Mac app.
|
|
106
|
+
|
|
107
|
+
- **Work.** Sort leads with today's meetings. A `#/work/sort` link opens Sort directly. The board holds still while you read: background loads no longer repaint under your finger.
|
|
108
|
+
- **Meetings.** Two columns from 860 px wide, so an unfolded phone reads like the Mac. The live transcript no longer jumps when new lines arrive. A Model menu picks the model for meeting Ask, and a refused change says why in words. When a sign-in lapses, every Meetings call stops and the reason stays on screen. Months that hit the list cap say N of M.
|
|
109
|
+
- **Sessions and Speakers hold still and say when offline.** Nothing above the turns moves while you read, Speakers never rebuilds the whole pane under your finger, and a read that times out offers Retry.
|
|
110
|
+
- **The sign-in screen keeps its message.** A later sign-out never blanks the words that say why you were signed out.
|
|
111
|
+
- Default-on changes: none beyond the web page itself; the Model menu changes the one meeting-assistance model the Mac also uses.
|
|
112
|
+
|
|
113
|
+
## 6.99.3 - Signing in on the phone says what went wrong (private)
|
|
114
|
+
|
|
115
|
+
2026-11-04. Private build for one Mac, on top of 6.99.2. Pairs with COS Control 0.5.367, which adds Open COS on your phone. Nothing changes for the glasses or the phone companion.
|
|
116
|
+
|
|
117
|
+
- **Fix: the sign-in page had no way in for a first phone.** A code only came from a browser that was already signed in, and the only code the Mac showed was the glasses pairing code, which the page refused. COS Control 0.5.367 adds Open COS on your phone: scan its QR with the phone camera and Safari opens signed in, or tap Show a sign-in code and type it. The server now gives Control that phone link: the handoff answer adds `phoneUrl` (this Mac's tailnet name, or its tailnet address when the name is not known yet) and `tailnet.ipUrl` / `tailnet.nameUrl`. The link works once and lasts 2 minutes.
|
|
118
|
+
- **The page names what failed.** A wrong token, a wrong or expired code, an expired sign-in link, and the glasses pairing code each get their own message. Typing the glasses code says it only pairs the COS Glasses app, and the code is left untouched for the glasses. That answer is given at most 20 times per glasses code across every device, the same cap the glasses app has, so the page cannot be used to hunt for the code. After 8 failures in 10 minutes the lockout message says what the last try was. Every failure still counts toward that limit. If the phone cannot reach the Mac at all, the page now says so instead of showing the browser's "Load failed".
|
|
119
|
+
- **A pasted token survives the paste.** The token is checked as typed, then again with spaces, line breaks, zero-width characters and wrapping quotes removed. The page has a Show token button and says the copy clears after 60 seconds.
|
|
120
|
+
- **The server log says why a sign-in failed** (`[web-login] fail kind=wrong_code len=8 ip=...`): the kind, the length and the device address, never what was typed.
|
|
121
|
+
|
|
122
|
+
## 6.99.2 - The phone address by name works (private)
|
|
123
|
+
|
|
124
|
+
2026-11-04. Private build for one Mac, on top of 6.99.1. Nothing changes for the glasses, the phone companion or COS Control.
|
|
125
|
+
|
|
126
|
+
- **Fix: signing in at `http://<your-mac>.<your-tailnet>.ts.net:3141/control` was refused.** The server learned this Mac's Tailscale name from the Tailscale command line, which does not answer when the server runs in the background, so every sign-in from the name address was turned away. It now asks Tailscale's own DNS for the name of this Mac's Tailscale address, which answers in milliseconds. Only this Mac's exact name is accepted. The address by number (`http://100.x.y.z:3141/control`) always worked.
|
|
127
|
+
|
|
128
|
+
## 6.99.1 - COS Control in a browser on your phone, over Tailscale (private)
|
|
129
|
+
|
|
130
|
+
2026-11-04. Private build for one Mac, on top of 6.99.0. Nothing changes for the glasses, the phone companion or COS Control.
|
|
131
|
+
|
|
132
|
+
- **COS Control Web.** Open `http://<your-mac>.<your-tailnet>.ts.net:3141/control` on a phone or another computer on your tailnet: Today, Sessions, Meetings, Work, Messages, Memories, Speakers, Usage and Settings, against the same routes the Mac app uses. Built phone first for the iPhone Duo: on the closed screen one top bar that slides away while you scroll; on the open screen a labelled left rail and two columns. No bottom bar, because Safari owns that edge.
|
|
133
|
+
- **Talk to your sessions from the phone.** Open a Claude Code, Codex or Cursor session and send it a message, the way the glasses' Continue does. If the Mac is busy with it, the message waits in that session's line and goes when it is free; if COS is still answering, it goes right after. Stop a running reply, or Fork to send your message to a copy. The reply streams in as it is written.
|
|
134
|
+
- **Sign in once per browser.** On a signed-in browser choose Settings, Sign in another device, then type the 8-character code on the phone (single use, five minutes). The pairing token still works. The session is an HttpOnly cookie for 7 days; the page never holds the token. Settings, Sign out every browser ends every browser session at once; the glasses and the phone app stay paired.
|
|
135
|
+
- **This Mac and your tailnet only.** The page and its sign-in answer loopback and Tailscale addresses. Home Wi-Fi is off unless `COS_WEB_CONTROL_LAN=1`. `COS_WEB_CONTROL_ENABLED=0` turns the whole thing off.
|
|
136
|
+
- **Guards.** Changes from a browser need the page's own origin, matching the address it reached, plus an extra header, so another site or another machine cannot act for you. Eight wrong codes or tokens in ten minutes lock that device out for the rest of the window. The page runs only its own scripts. A browser session can do what the pairing token can, so sign out a browser you no longer use.
|
|
137
|
+
- New routes: `/api/web-session/{login,logout,status,device-code,handoff,revoke-all}`, `/api/tailscale/web-url`, `/api/web-bridge/memories`, `/api/meetings/live-chunks`, `/api/usage/summary`, `/api/work-board/your-move`. Not in this build: answering session questions and approvals from the browser.
|
|
138
|
+
|
|
139
|
+
## 6.99.0 - Rename a live meeting, and up to 50 cues per meeting (private)
|
|
140
|
+
|
|
141
|
+
2026-11-03. Private build for one Mac, on top of the public 6.98.1. COS Control needs a later build to show the rename field; nothing changes for older clients.
|
|
142
|
+
|
|
143
|
+
- A live meeting (a G2 meeting or a Mac recording) can be renamed while it records: `GET` and `PUT /api/meeting/title-steer`. The name is kept across a server restart, a second client editing at the same time is told the name changed, and the name text is never logged.
|
|
144
|
+
- When the meeting saves without a title of its own, the name you gave becomes its title and helps file it in the right domain. It is also saved as a title steer, so the meeting sync names the meeting from your words and sharpens them, instead of starting from "Mac recording 9:05 PM".
|
|
145
|
+
- A placeholder title such as "Mac recording 9:05 PM" never beats a name you gave; a real title sent with the save still wins. A rename sent while the meeting is saving is refused (409 `save_in_progress`) instead of being lost.
|
|
146
|
+
- The live meeting file Control reads carries the name as soon as you change it. `capabilities.titleSteer` says a server supports it.
|
|
147
|
+
- Live cues per meeting can be set up to 50 (was 25) in COS Control or the phone. The default stays 25. A meeting at 50 can use up to 100 of the 150 daily writer calls.
|
|
148
|
+
- `GET /live-cues/history` now reports the per-meeting cap you set, not a fixed 25.
|
|
149
|
+
|
|
1
150
|
## 6.98.1 - Tableau answers by lead source (private)
|
|
2
151
|
|
|
3
152
|
2026-11-03. Private build for one Mac, on top of the public 6.98.0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.99.12",
|
|
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": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"shared",
|
|
42
42
|
"!server/**/*.test.ts",
|
|
43
43
|
"!server/scripts/engine-canary.ts",
|
|
44
|
+
"!server/scripts/start-web-candidate.mjs",
|
|
44
45
|
"!server/scripts/hq-native-canary.ts",
|
|
45
46
|
"!server/scripts/hq-contention-canary.ts",
|
|
46
47
|
"!server/scripts/live-cos-ask-canary.ts",
|
package/server/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { randomBytes, randomUUID } from 'node:crypto'
|
|
|
15
15
|
import { healthRouter } from './routes/health.js'
|
|
16
16
|
import { diagRouter } from './routes/diag.js'
|
|
17
17
|
import { createClientInstanceRouter, isMeetingChunkPost } from './routes/client-instance.js'
|
|
18
|
-
import { installTailscaleNudger } from './lib/tailscale-nudge.js'
|
|
18
|
+
import { getTailscaleNudger, installTailscaleNudger } from './lib/tailscale-nudge.js'
|
|
19
19
|
import { queryRouter } from './routes/query.js'
|
|
20
20
|
import { providerProofRouter } from './routes/provider-proof.js'
|
|
21
21
|
import { transcribeRouter } from './routes/transcribe.js'
|
|
@@ -123,7 +123,7 @@ import { meetingAskRouter } from './routes/meeting-ask.js'
|
|
|
123
123
|
import { meetingAskRuntime } from './lib/meeting-ask-runtime.js'
|
|
124
124
|
import { lensGistRouter } from './routes/lens-gist.js'
|
|
125
125
|
import { bookmarksRouter } from './routes/bookmarks.js'
|
|
126
|
-
import { setWeatherPersistence, welcomeContextRouter } from './routes/welcome-context.js'
|
|
126
|
+
import { fetchNextEvent, setWeatherPersistence, welcomeContextRouter } from './routes/welcome-context.js'
|
|
127
127
|
import { liveCuesRouter } from './routes/live-cues.js'
|
|
128
128
|
import { tasksRouter } from './routes/tasks.js'
|
|
129
129
|
import { memoryRouter } from './routes/memory.js'
|
|
@@ -169,6 +169,20 @@ import { createWorkOrchestratorRouter } from './routes/work-orchestrator.js'
|
|
|
169
169
|
import { createWorkLensRoundsRouter } from './routes/work-lens-rounds.js'
|
|
170
170
|
import { startWorkOrchestrator } from './lib/work-orchestrator/runtime.js'
|
|
171
171
|
import { createWorkSearchRouter } from './routes/work-search.js'
|
|
172
|
+
import { createDropsRouter, lazyDropService, openDropService } from './routes/drops.js'
|
|
173
|
+
import { createDropRouter, sharedDropJevClient, dropDailyCap, dropRouterEnabled } from './lib/drop-router.js'
|
|
174
|
+
import { createBridgeDropWriters } from './lib/drop-destinations.js'
|
|
175
|
+
import { dropItEnabled } from './lib/drop-settings.js'
|
|
176
|
+
import { serverTimezone } from './lib/morning-brief-config.js'
|
|
177
|
+
import { createYourMoveCounter } from './lib/your-move-count.js'
|
|
178
|
+
import { readWorkJournal } from './lib/work-activity.js'
|
|
179
|
+
import { createDropP2Boot } from './lib/drop-p2-boot.js'
|
|
180
|
+
import { createP2SettingsReader } from './lib/drop-p2-settings.js'
|
|
181
|
+
import { createDropP2Runtime, journalReceiptsLite } from './lib/drop-p2-runtime.js'
|
|
182
|
+
import { sessionTurnOutcome } from './lib/drop-resolution.js'
|
|
183
|
+
import { DropHistorySearcher } from './lib/drop-history-search.js'
|
|
184
|
+
import { emitDisplay } from './lib/display-bus.js'
|
|
185
|
+
import { COS_SCRIPTS_DIR } from './lib/python-bridge.js'
|
|
172
186
|
import { isReadOnlyApiPost } from './lib/work-search.js'
|
|
173
187
|
import { isMacVoiceNamingBatchPost } from './lib/mac-voice-naming-batch.js'
|
|
174
188
|
import { dataPath } from './lib/data-dir.js'
|
|
@@ -198,7 +212,10 @@ import {
|
|
|
198
212
|
import { isPublicApiRequest, requireApiToken } from './lib/api-auth.js'
|
|
199
213
|
import { PairingState, pairingHostsFrom } from './lib/glasses-pairing.js'
|
|
200
214
|
import { createPairingRouter } from './routes/pairing.js'
|
|
201
|
-
import {
|
|
215
|
+
import { createWebControlRouter, isAllowedCorsOrigin, webControlCsp, webControlEnabled } from './routes/web-control.js'
|
|
216
|
+
import { isWebControlSocketAllowed, isWebSessionAuthorized, setWebSessionEpochFile } from './lib/web-session-auth.js'
|
|
217
|
+
import { getTailnetSelfName, installTailnetSelfName } from './lib/tailnet-self-name.js'
|
|
218
|
+
import { isManagedRuntime, managedServerVersion } from './lib/managed-runtime.js'
|
|
202
219
|
import { reportClaudeExtraToolConfiguration } from './lib/claude-tool-access.js'
|
|
203
220
|
import {
|
|
204
221
|
acquireMaintenanceWork,
|
|
@@ -271,13 +288,17 @@ app.use((req, res, next) => {
|
|
|
271
288
|
next()
|
|
272
289
|
})
|
|
273
290
|
|
|
274
|
-
// Allow localhost + LAN IPs (glasses WebView accesses server over LAN)
|
|
291
|
+
// Allow localhost + LAN IPs (glasses WebView accesses server over LAN) + this Mac's verified Tailscale MagicDNS
|
|
275
292
|
app.use(cors({
|
|
276
293
|
origin: (origin, cb) => {
|
|
277
294
|
// Allow requests with no origin (same-origin, curl, SSE) or "null" origin (file:// WebViews like Even Hub)
|
|
278
295
|
if (!origin || origin === 'null') return cb(null, true)
|
|
279
|
-
|
|
280
|
-
|
|
296
|
+
// Async only for an unrecognised *.ts.net origin (see isAllowedCorsOrigin); every other
|
|
297
|
+
// origin is decided from cached state, as before.
|
|
298
|
+
isAllowedCorsOrigin(origin, getTailnetSelfName()).then(
|
|
299
|
+
ok => ok ? cb(null, true) : cb(new Error('CORS blocked')),
|
|
300
|
+
() => cb(new Error('CORS blocked')),
|
|
301
|
+
)
|
|
281
302
|
},
|
|
282
303
|
// Without this, a cross-origin reader sees only the CORS-safelisted response
|
|
283
304
|
// headers. The companion's origin is `null` (a file:// WebView), so it is
|
|
@@ -367,16 +388,31 @@ const pairingState = new PairingState({
|
|
|
367
388
|
// request that arrives proves the tunnel carries COS traffic. Authenticated ones also mark the
|
|
368
389
|
// address as a paired phone. Notes only, never answers; noteRequest never throws.
|
|
369
390
|
const tailscaleNudger = installTailscaleNudger({ activeMeetings: () => getActiveTranscriptionSessionCount() })
|
|
391
|
+
// COS Control Web: this Mac's MagicDNS name, from the nudger when it has one and otherwise
|
|
392
|
+
// from a reverse lookup at MagicDNS (the CLI does not answer under this LaunchAgent; see
|
|
393
|
+
// lib/tailnet-self-name.ts). Learned at boot so the first phone sign-in is not refused.
|
|
394
|
+
const tailnetSelf = installTailnetSelfName({ nudgerName: () => tailscaleNudger.currentSelfDnsName() })
|
|
395
|
+
if (webControlEnabled()) void tailnetSelf.refresh()
|
|
396
|
+
// Browser sessions sign this Mac's session epoch; "Sign out every browser" bumps it.
|
|
397
|
+
setWebSessionEpochFile(dataPath('web-session.json'))
|
|
370
398
|
app.use('/api', (req, _res, next) => {
|
|
371
399
|
tailscaleNudger.noteRequest(req.socket?.remoteAddress, { meetingChunk: isMeetingChunkPost(req.method, req.path) })
|
|
372
400
|
next()
|
|
373
401
|
})
|
|
402
|
+
const HTTPS_PORT = parseInt(process.env.HTTPS_PORT ?? '3143', 10)
|
|
374
403
|
app.use('/api', requireApiToken(API_TOKEN, {
|
|
375
404
|
onAuthenticated: req => pairingState.noteAuthenticated(req.socket?.remoteAddress),
|
|
405
|
+
selfDnsName: () => tailnetSelf.currentSelfDnsName(),
|
|
406
|
+
allowedPorts: () => [PORT, HTTPS_PORT],
|
|
376
407
|
}))
|
|
377
408
|
// Past the token gate, a request on a non-public path carried the token: mark a paired phone.
|
|
409
|
+
// Only the X-Cos-Token header counts: a browser signed in to COS Control Web passed the gate
|
|
410
|
+
// with its cookie, and treating that laptop or tab as a paired phone would point the nudger's
|
|
411
|
+
// phone checks at it (review note).
|
|
378
412
|
app.use('/api', (req, _res, next) => {
|
|
379
|
-
if (!isPublicApiRequest(req.method, req.path)
|
|
413
|
+
if (!isPublicApiRequest(req.method, req.path) && typeof req.headers['x-cos-token'] === 'string') {
|
|
414
|
+
tailscaleNudger.noteRequest(req.socket?.remoteAddress, { authenticated: true })
|
|
415
|
+
}
|
|
380
416
|
next()
|
|
381
417
|
})
|
|
382
418
|
|
|
@@ -854,6 +890,12 @@ app.use('/api', createWorkReviewsRouter(workReviewRuntime))
|
|
|
854
890
|
const workHandoffRequestStore = createOptionalWorkHandoffRequestStore(() => new WorkHandoffRequestStore(dataPath('work-handoff-requests')))
|
|
855
891
|
// One bounded board read (5 s) shared by the glasses' Work glances and the request inbox; GET /api/work-board primes it.
|
|
856
892
|
const workBoardReader = createWorkBoardReader(() => listBoard())
|
|
893
|
+
// Drop it P2: a card written through this server kicks the resolution poll (a few seconds, one poll per burst) instead
|
|
894
|
+
// of waiting up to 2 minutes. Mounted before the Work and tasks routers so it sees their answers; it never changes them.
|
|
895
|
+
app.use(['/api/work-board', '/api/tasks'], (req, res, next) => {
|
|
896
|
+
if (req.method !== 'GET') res.on('finish', () => { if (res.statusCode < 300) dropP2.runtime()?.kickPoll() })
|
|
897
|
+
next()
|
|
898
|
+
})
|
|
857
899
|
app.use('/api', createWorkBoardRouter({ readBoard: workBoardReader, requests: () => handoffRequestCapabilities(workHandoffRequestStore, Date.now()) }))
|
|
858
900
|
app.use('/api', createWorkHandoffRequestsRouter({ store: workHandoffRequestStore, readBoard: workBoardReader }))
|
|
859
901
|
// Work intake (6.57.0): meeting-derived suggestions, asks and review items. Optional: a bad journal never blocks boot.
|
|
@@ -866,6 +908,67 @@ app.use('/api', createJevRouter(workReviewRuntime ? { review: async id => workRe
|
|
|
866
908
|
app.use('/api', createDecisionProviderRouter())
|
|
867
909
|
app.use('/api', createSubqKeyRouter())
|
|
868
910
|
app.use('/api', createWorkSearchRouter())
|
|
911
|
+
// Drop it P1 (6.100 candidate): the pinned COS chat's drops, receipts and History. Off by default (COS_DROP_IT or
|
|
912
|
+
// PUT /api/drops/settings); its stores open on first use, so an install with it off writes nothing. The router is one
|
|
913
|
+
// Jev request per drop with its own cap (COS_DROP_DAILY_TOKENS), ledger, breaker and switch (COS_DROP_ROUTER).
|
|
914
|
+
// 6.99.11: P2 settings (data/drop-p2-settings.json), read through a 15 s cache; a PUT updates it at once.
|
|
915
|
+
const dropP2Settings = createP2SettingsReader()
|
|
916
|
+
const dropService = lazyDropService(() => openDropService(part => dataPath(part), {
|
|
917
|
+
p2Settings: () => dropP2Settings.read(),
|
|
918
|
+
router: createDropRouter(),
|
|
919
|
+
writers: createBridgeDropWriters(),
|
|
920
|
+
timezone: () => serverTimezone(),
|
|
921
|
+
askLookup: async (clientJobId, generation) => {
|
|
922
|
+
const job = await queryJobCoordinator.getByClientGeneration(clientJobId, generation)
|
|
923
|
+
return job ? { status: job.status, response: job.response, sessionId: job.sessionId } : undefined
|
|
924
|
+
},
|
|
925
|
+
}))
|
|
926
|
+
// Drop it P2 (6.99.11 candidate): pings, reminders and follow-ups, running only while Drop it is on (checked at boot and
|
|
927
|
+
// each minute). It reads signals that already exist (calendar, Work intake, the board, the Work journal, the thread and
|
|
928
|
+
// signal caches) and calls no model; History search by meaning shares the drop router's Jev client and cap.
|
|
929
|
+
const dropP2 = createDropP2Boot({
|
|
930
|
+
enabled: () => dropItEnabled(),
|
|
931
|
+
service: dropService,
|
|
932
|
+
pingRoot: dataPath('pings'),
|
|
933
|
+
make: (stores, pingState) => createDropP2Runtime({
|
|
934
|
+
...stores, pingState,
|
|
935
|
+
timezone: () => serverTimezone(),
|
|
936
|
+
nextEvent: async () => { const r = await fetchNextEvent(); return r.read ? r.event : undefined },
|
|
937
|
+
intakeOpen: () => workIntakeStore?.open(),
|
|
938
|
+
scriptsDir: COS_SCRIPTS_DIR,
|
|
939
|
+
board: () => workBoardReader.read(60_000, 5_000),
|
|
940
|
+
journal: () => { const j = readWorkJournal(); return j.available ? journalReceiptsLite(j.receipts) : undefined },
|
|
941
|
+
emitLens: event => { emitDisplay(event) },
|
|
942
|
+
// Quiet while the server drains for an update or restart, as the morning brief is.
|
|
943
|
+
paused: () => gracefulShutdownStarted || !maintenanceAdmissionsOpen(),
|
|
944
|
+
settings: () => dropP2Settings.read(),
|
|
945
|
+
}),
|
|
946
|
+
})
|
|
947
|
+
// The turn after Send to session ended: its held drop gets one follow-up (replied, or stopped). Never throws.
|
|
948
|
+
sessionSignalStore.subscribe((signal, env, child) => {
|
|
949
|
+
try {
|
|
950
|
+
const outcome = sessionTurnOutcome(env.event, child)
|
|
951
|
+
if (outcome) dropP2.runtime()?.onSessionTurn(signal.sessionId, outcome, env.ts)
|
|
952
|
+
} catch { /* a follow-up is never worth breaking another subscriber */ }
|
|
953
|
+
})
|
|
954
|
+
const dropSearcher = new DropHistorySearcher(sharedDropJevClient())
|
|
955
|
+
app.use('/api', createDropsRouter({
|
|
956
|
+
enabled: () => dropItEnabled(),
|
|
957
|
+
service: dropService,
|
|
958
|
+
p2: () => dropP2.runtime(),
|
|
959
|
+
search: () => dropSearcher,
|
|
960
|
+
p2Settings: dropP2Settings,
|
|
961
|
+
routerStatus: () => {
|
|
962
|
+
const status = sharedDropJevClient().status()
|
|
963
|
+
return { on: dropRouterEnabled(), dailyCap: dropDailyCap(), usedToday: status.usedToday, breakerOpenUntil: status.breakerOpenUntil }
|
|
964
|
+
},
|
|
965
|
+
// 6.99.10: Work items in Your move count toward `waiting` (plan section 12). The board read is the one Work's own
|
|
966
|
+
// glances share (10 s old at most, 3 s bound), and the count is kept 30 s, so the summary poll adds no bridge load.
|
|
967
|
+
yourMove: createYourMoveCounter({
|
|
968
|
+
journal: () => readWorkJournal(),
|
|
969
|
+
rows: () => workBoardReader.read(10_000, 3_000).catch(() => null),
|
|
970
|
+
}),
|
|
971
|
+
}))
|
|
869
972
|
// Find a session by meaning (Control 0.5.363): consent per provider, its own ledgers and breakers (lib/session-find.ts).
|
|
870
973
|
app.use('/api', createSessionFindRouter())
|
|
871
974
|
app.use('/api', createWorkChatRouter())
|
|
@@ -898,7 +1001,12 @@ app.use('/api', createSessionHooksRouter({ port: PORT }))
|
|
|
898
1001
|
// 6.52.0: the permission broker's client API (the hook's door is mounted before the /api
|
|
899
1002
|
// gate, above). Behind the API token; a poll counts as a live answerer only with
|
|
900
1003
|
// `?client=glasses|phone`.
|
|
901
|
-
|
|
1004
|
+
// 6.99.12: COS Control Web reads the questions with its browser session, checked exactly as the /api gate checks it.
|
|
1005
|
+
const webSessionCookieAuthorized = (req: express.Request): boolean => isWebSessionAuthorized(req, API_TOKEN, {
|
|
1006
|
+
selfDnsName: tailnetSelf.currentSelfDnsName(),
|
|
1007
|
+
allowedPorts: [PORT, HTTPS_PORT],
|
|
1008
|
+
})
|
|
1009
|
+
app.use('/api', createSessionQuestionsRouter({ broker: permissionBroker, apiToken: () => API_TOKEN, webSessionAuthorized: webSessionCookieAuthorized }))
|
|
902
1010
|
// Phase 0 of Continue Original Agent Thread: can COS write into a desktop thread
|
|
903
1011
|
// without colliding with a live writer? Read-only — it answers, it never attaches.
|
|
904
1012
|
// Registered AFTER agentSessionsRouter deliberately: its paths are 2 and 4 segments
|
|
@@ -1305,6 +1413,7 @@ async function gracefulShutdown(): Promise<void> {
|
|
|
1305
1413
|
stopRoutineScheduler()
|
|
1306
1414
|
tailscaleNudger.stop()
|
|
1307
1415
|
await workReviewRuntime?.close()
|
|
1416
|
+
dropP2.stop()
|
|
1308
1417
|
workIntakeStore?.close()
|
|
1309
1418
|
workHandoffRequestStore?.close()
|
|
1310
1419
|
stopMeetingImportScheduler()
|
|
@@ -1361,6 +1470,34 @@ process.on('unhandledRejection', (reason: any) => {
|
|
|
1361
1470
|
// Start HTTPS alongside HTTP — Even Hub WebView prefers HTTPS (iOS ATS).
|
|
1362
1471
|
// Optional: drop cert.pem + key.pem in server/certs/ (e.g. via mkcert) to enable.
|
|
1363
1472
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
1473
|
+
if (webControlEnabled()) {
|
|
1474
|
+
const webControlDir = path.join(__dirname, 'web-control')
|
|
1475
|
+
app.use('/control', (req, res, next) => {
|
|
1476
|
+
// Loopback and Tailscale only, unless COS_WEB_CONTROL_LAN=1 (review W2).
|
|
1477
|
+
if (!isWebControlSocketAllowed(req.socket?.remoteAddress, process.env, req.headers)) {
|
|
1478
|
+
res.status(403).type('text').send('COS Control Web is available on this Mac and over Tailscale.')
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
res.setHeader('Content-Security-Policy', webControlCsp(req.path))
|
|
1482
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
1483
|
+
res.setHeader('Referrer-Policy', 'no-referrer')
|
|
1484
|
+
next()
|
|
1485
|
+
}, express.static(webControlDir))
|
|
1486
|
+
app.use('/api', createWebControlRouter({
|
|
1487
|
+
apiToken: () => API_TOKEN,
|
|
1488
|
+
serverVersion: () => managedServerVersion() ?? 'unknown',
|
|
1489
|
+
port: () => PORT,
|
|
1490
|
+
httpsPort: () => existsSync(path.join(__dirname, 'certs', 'cert.pem')) ? HTTPS_PORT : null,
|
|
1491
|
+
selfDnsName: () => tailnetSelf.currentSelfDnsName(),
|
|
1492
|
+
// A glasses pairing code typed on the web sign-in page gets its own message (read only).
|
|
1493
|
+
glassesCodeIsLive: code => pairingState.isLiveCode(code),
|
|
1494
|
+
liveGlassesCodeKey: () => pairingState.status().code?.expiresAt ?? null,
|
|
1495
|
+
queryTailscaleStatus: async () => {
|
|
1496
|
+
const name = await tailnetSelf.refresh()
|
|
1497
|
+
return name ? { backendState: 'Running', selfIps: [], selfDnsName: name, certDomains: [] } : null
|
|
1498
|
+
},
|
|
1499
|
+
}))
|
|
1500
|
+
}
|
|
1364
1501
|
/**
|
|
1365
1502
|
* Let a slow large upload finish instead of killing its socket mid-body.
|
|
1366
1503
|
*
|
|
@@ -1389,7 +1526,6 @@ function withRequestTimeout<T extends { requestTimeout: number }>(server: T): T
|
|
|
1389
1526
|
return server
|
|
1390
1527
|
}
|
|
1391
1528
|
|
|
1392
|
-
const HTTPS_PORT = parseInt(process.env.HTTPS_PORT ?? '3143', 10)
|
|
1393
1529
|
const certDir = path.join(__dirname, 'certs')
|
|
1394
1530
|
const listeners: RequiredListener[] = []
|
|
1395
1531
|
if (existsSync(path.join(certDir, 'cert.pem'))) {
|
|
@@ -1604,6 +1740,8 @@ listenRequiredServers(listeners).then(() => {
|
|
|
1604
1740
|
// still mounted, so the kill switch is an immediate rollback.
|
|
1605
1741
|
console.error('[COS API] Durable query-job store unavailable:', error)
|
|
1606
1742
|
})
|
|
1743
|
+
// Drop it P2: off unless Drop it is on; the boot re-checks the switch each minute.
|
|
1744
|
+
dropP2.start()
|
|
1607
1745
|
|
|
1608
1746
|
if (COS_MODE) {
|
|
1609
1747
|
initSessionCache()
|
package/server/lib/api-auth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RequestHandler } from 'express'
|
|
2
2
|
import { timingSafeTokenEqual } from './token-auth.js'
|
|
3
3
|
import { PAIRING_NONCE_BODY } from './glasses-pairing.js'
|
|
4
|
+
import { isWebSessionAuthorized } from './web-session-auth.js'
|
|
4
5
|
|
|
5
6
|
// Recovery/setup clients need these availability surfaces before they have a
|
|
6
7
|
// usable token. Keep private provider state and every mutation route out.
|
|
@@ -47,6 +48,11 @@ export function isPublicApiRequest(method: string, path: string): boolean {
|
|
|
47
48
|
if (PUBLIC_API_PATHS.has(path)) return true
|
|
48
49
|
if (method === 'POST' && path === '/pairing/claim') return true
|
|
49
50
|
if (method === 'GET' && PAIRING_POLL_PATH.test(path)) return true
|
|
51
|
+
// COS Control Web (browser on the tailnet). Login takes the token or a one-time handoff
|
|
52
|
+
// ticket and answers with the HttpOnly cookie; it is rate limited in the route. Status
|
|
53
|
+
// says only whether this browser is signed in. Everything else stays behind the gate.
|
|
54
|
+
if (method === 'POST' && (path === '/web-session/login' || path === '/web-session/logout')) return true
|
|
55
|
+
if (method === 'GET' && path === '/web-session/status') return true
|
|
50
56
|
if (method !== 'GET' && method !== 'HEAD') return false
|
|
51
57
|
return TTS_PLAYBACK_CAPABILITY_PATH.test(path)
|
|
52
58
|
|| DISPLAY_STREAM_CAPABILITY_PATH.test(path)
|
|
@@ -54,29 +60,40 @@ export function isPublicApiRequest(method: string, path: string): boolean {
|
|
|
54
60
|
|
|
55
61
|
export interface RequireApiTokenOptions {
|
|
56
62
|
/**
|
|
57
|
-
* Called once per request that passed the
|
|
58
|
-
*
|
|
59
|
-
*
|
|
63
|
+
* Called once per request that passed the X-COS-Token header check (never for a
|
|
64
|
+
* public path and never for a browser cookie session so web login cannot disturb
|
|
65
|
+
* the glasses pairing state machine).
|
|
60
66
|
*/
|
|
61
67
|
onAuthenticated?: (req: Parameters<RequestHandler>[0]) => void
|
|
68
|
+
/** Verified Tailscale `Self.DNSName` for validating browser cookie Origins. */
|
|
69
|
+
selfDnsName?: () => string | null
|
|
70
|
+
/** Allowed browser-facing server ports (e.g. `[3141, 3143]`) for cookie Origin port binding. */
|
|
71
|
+
allowedPorts?: () => readonly number[]
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/** Global /api authentication boundary. Mount before all body parsers. */
|
|
65
75
|
export function requireApiToken(apiToken: string, options: RequireApiTokenOptions = {}): RequestHandler {
|
|
66
76
|
return (req, res, next) => {
|
|
67
77
|
if (isPublicApiRequest(req.method, req.path)) return next()
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Older COS Glasses builds, which cannot scan, show this message word for word.
|
|
74
|
-
message: 'Paste the pairing token from COS Control, or scan its code with COS Glasses 6.10.621 or newer.',
|
|
75
|
-
})
|
|
78
|
+
if (timingSafeTokenEqual(req.headers['x-cos-token'], apiToken)) {
|
|
79
|
+
if (options.onAuthenticated) {
|
|
80
|
+
try { options.onAuthenticated(req) } catch { /* observer only */ }
|
|
81
|
+
}
|
|
82
|
+
return next()
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
const cookieAuth = isWebSessionAuthorized(req, apiToken, {
|
|
85
|
+
selfDnsName: options.selfDnsName?.() ?? null,
|
|
86
|
+
allowedPorts: options.allowedPorts?.(),
|
|
87
|
+
})
|
|
88
|
+
if (cookieAuth) {
|
|
89
|
+
return next()
|
|
79
90
|
}
|
|
80
|
-
|
|
91
|
+
return res.status(401).json({
|
|
92
|
+
error: 'unauthorized',
|
|
93
|
+
// Stable on purpose: clients key off this value, not the message.
|
|
94
|
+
reason: 'pairing_token_rejected',
|
|
95
|
+
// Older COS Glasses builds, which cannot scan, show this message word for word.
|
|
96
|
+
message: 'Paste the pairing token from COS Control, or scan its code with COS Glasses 6.10.621 or newer.',
|
|
97
|
+
})
|
|
81
98
|
}
|
|
82
99
|
}
|
|
@@ -418,6 +418,8 @@ const PHASE_LABELS: Record<Phase, string> = {
|
|
|
418
418
|
* Returns the session ID for multi-turn tracking.
|
|
419
419
|
*/
|
|
420
420
|
export interface CallOptions {
|
|
421
|
+
/** Internal checkpoint runner only; timeout is never a successful answer. */
|
|
422
|
+
maintenancePass?: true
|
|
421
423
|
lightweight?: boolean // Skip async context fetch — use cached context instantly (G2 speed path)
|
|
422
424
|
abortSignal?: AbortSignal
|
|
423
425
|
effort?: EffortPreference
|
|
@@ -700,7 +702,7 @@ export async function callClaudeStreaming(
|
|
|
700
702
|
const effortWallMax = resolvedEffort === 'max' || resolvedEffort === 'ultracode'
|
|
701
703
|
? Math.max(defaultWallMax, WALL_MAX_DEEP_EFFORT_MS)
|
|
702
704
|
: defaultWallMax
|
|
703
|
-
const wallMax = isExtendedQuery(query) ? Math.max(WALL_MAX_EXTENDED_MS, effortWallMax) : effortWallMax
|
|
705
|
+
const wallMax = options?.maintenancePass ? 900_000 : isExtendedQuery(query) ? Math.max(WALL_MAX_EXTENDED_MS, effortWallMax) : effortWallMax
|
|
704
706
|
// Key lanes: one nonce per turn from the relay (P2 contract), then the child env points at it.
|
|
705
707
|
// Only the child's copy is written. A relay that cannot issue ends the turn before spawn.
|
|
706
708
|
let keyTurn: RelayTurnHandle | null = null
|
|
@@ -850,8 +852,16 @@ export async function callClaudeStreaming(
|
|
|
850
852
|
})
|
|
851
853
|
}
|
|
852
854
|
|
|
855
|
+
let maintenanceClosed = false
|
|
853
856
|
async function finalize(text: string) {
|
|
854
857
|
if (finalized) return
|
|
858
|
+
// A next maintenance pass may start only after the entire provider tree
|
|
859
|
+
// has closed. Normal chat retains its existing result-event behavior.
|
|
860
|
+
if (options?.maintenancePass && !maintenanceClosed) {
|
|
861
|
+
if (terminationRequested) return
|
|
862
|
+
await terminateForTerminal('maintenance checkpoint', () => finalize(text))
|
|
863
|
+
return
|
|
864
|
+
}
|
|
855
865
|
const responseAuthFailure = terminalProviderAuthFailure('claude', text)
|
|
856
866
|
const authFailure = responseAuthFailure
|
|
857
867
|
?? (!text.trim() ? terminalTextError ?? terminalProviderAuthFailure('claude', stderr) : null)
|
|
@@ -1010,6 +1020,11 @@ export async function callClaudeStreaming(
|
|
|
1010
1020
|
status: Exclude<ClaudeRunStatus, 'running'> = 'failed',
|
|
1011
1021
|
) {
|
|
1012
1022
|
if (finalized) return
|
|
1023
|
+
if (options?.maintenancePass && !maintenanceClosed) {
|
|
1024
|
+
if (terminationRequested) return
|
|
1025
|
+
await terminateForTerminal('maintenance error', () => finalizeError(msg, exitCode, status))
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1013
1028
|
finalized = true
|
|
1014
1029
|
cleanup()
|
|
1015
1030
|
armSteerReaper()
|
|
@@ -1041,6 +1056,7 @@ export async function callClaudeStreaming(
|
|
|
1041
1056
|
console.error(`[claude-bridge] provider did not close after SIGKILL (${reason}); retaining lifecycle ownership`)
|
|
1042
1057
|
return
|
|
1043
1058
|
}
|
|
1059
|
+
maintenanceClosed = true
|
|
1044
1060
|
await onClosed(result)
|
|
1045
1061
|
}
|
|
1046
1062
|
|
|
@@ -1117,6 +1133,7 @@ export async function callClaudeStreaming(
|
|
|
1117
1133
|
|
|
1118
1134
|
const wallTimer = setTimeout(() => {
|
|
1119
1135
|
void terminateForTerminal('wall timeout', result => {
|
|
1136
|
+
if (options?.maintenancePass) return finalizeError('maintenance_pass_timeout', result.code)
|
|
1120
1137
|
// 6.78.0 steer: a steered run past its first whole answer completes with that answer.
|
|
1121
1138
|
if (steerCheckpoint !== null) return finalize(steerCheckpoint)
|
|
1122
1139
|
if (fullText) {
|
|
@@ -1338,7 +1355,9 @@ export async function callClaudeStreaming(
|
|
|
1338
1355
|
} catch { /* ignore */ }
|
|
1339
1356
|
}
|
|
1340
1357
|
|
|
1341
|
-
if (
|
|
1358
|
+
if (options?.maintenancePass) {
|
|
1359
|
+
void finalizeError('maintenance_pass_incomplete_exit', code)
|
|
1360
|
+
} else if (fullText) {
|
|
1342
1361
|
// If we got text but no explicit result event, still finalize. A sole,
|
|
1343
1362
|
// machine-shaped auth response is classified inside finalize.
|
|
1344
1363
|
void finalize(fullText)
|