@gotcos/glasses-server 7.0.30 → 7.0.34
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/.env.example +10 -8
- package/CHANGELOG.md +207 -0
- package/bin/hooks/cos-session-hook +3 -2
- package/docs/pairing-fixture.json +1 -1
- package/package.json +3 -1
- package/server/index.ts +4 -2
- package/server/lib/api-auth.ts +3 -2
- package/server/lib/archive.ts +4 -2
- package/server/lib/claude-hooks-installer.ts +101 -15
- package/server/lib/codex-hooks-installer.ts +4 -1
- package/server/lib/full-hold.ts +61 -5
- package/server/lib/glasses-pairing.ts +2 -1
- package/server/lib/permission-broker.ts +130 -14
- package/server/lib/question-hold-setting.ts +38 -0
- package/server/lib/question-queue.ts +111 -0
- package/server/lib/session-hooks-runtime.ts +28 -3
- package/server/lib/session-title-cache.ts +40 -0
- package/server/lib/turn-end-ask.ts +51 -1
- package/server/lib/web-session-auth.ts +17 -0
- package/server/routes/agent-sessions.ts +3 -0
- package/server/routes/permission-broker.ts +31 -2
- package/server/routes/session-hooks.ts +23 -0
- package/server/routes/sessions.ts +3 -0
- package/server/web-control/.manifest.json +10 -10
- package/server/web-control/app.css +26 -3
- package/server/web-control/app.js +381 -36
- package/server/web-control/control.css +5 -1
- package/server/web-control/index.html +7 -5
- package/server/web-control/panes/cos-chat.css +21 -0
- package/server/web-control/panes/memories.js +18 -9
- package/server/web-control/panes/messages.js +15 -3
- package/server/web-control/panes/questions.js +6 -1
- package/server/web-control/panes/settings.js +18 -10
- package/server/web-control/shell.js +8 -13
package/.env.example
CHANGED
|
@@ -213,14 +213,16 @@ BIND_HOST=0.0.0.0
|
|
|
213
213
|
# (glasses, phone, COS Control, Control Web). Question text only, never options or answers. It is
|
|
214
214
|
# also written to the session-hook ledger on disk. Default ON; 0 restores 7.0.13's empty line.
|
|
215
215
|
# COS_WAITING_QUESTION_WORDS=0
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
216
|
+
# Hold a session question (QUESTION) or a tool approval (APPROVAL) for the whole hook wait, so the
|
|
217
|
+
# glasses, the phone or Control Web can answer it even if none was connected when it was asked. Only
|
|
218
|
+
# on engines proven to keep their own dialog up meanwhile (Claude Code CLI 2.1.321+ in a terminal,
|
|
219
|
+
# Claude Desktop 2.1.317+); the Mac's dialog still shows at once and the first answer wins.
|
|
220
|
+
# QUESTION, 7.0.34: ON BY DEFAULT, up to 8 hours, but only once COS Control > Sessions > Reinstall
|
|
221
|
+
# hooks has written the 8 hour question hook; until then nothing changes. 0 turns it off (then
|
|
222
|
+
# restart and Reinstall hooks); 1 turns it on outright. This line beats COS Control's setting.
|
|
223
|
+
# APPROVAL: OFF by default (1 hour when on). Keep it off for now: a signed-in Control Web browser
|
|
224
|
+
# could allow a tool for about an hour.
|
|
225
|
+
# COS_QUESTION_FULL_HOLD=0
|
|
224
226
|
# COS_APPROVAL_FULL_HOLD=1
|
|
225
227
|
# 7.0.18, default ON (named by Miles 11/10): a Claude desktop session that ends its turn with a
|
|
226
228
|
# question and reply buttons shows as waiting on the glasses, phone, Control Web and COS Control once
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,208 @@
|
|
|
1
|
+
## 7.0.34 - Questions can wait 8 hours, on by default (takes effect when you choose Reinstall hooks)
|
|
2
|
+
|
|
3
|
+
2026-11-13. 7.0.33 plus one change, the 8 hour question hold on by default. Miles, 15:02: "I want the 8 hour question hold ON. please keep it on by default." Everything in 7.0.33 (and in 7.0.32, which never reached npm) is in this update too; see their entries below.
|
|
4
|
+
|
|
5
|
+
**On for everyone, and what that means:**
|
|
6
|
+
- **If you do nothing, one line is new; nothing else changes.** After updating, your Claude Code hooks, your questions' wait (about 10 minutes, and only while a device is with you), what reaches your browser and how you answer there are all exactly 7.0.33 with the switch off, and COS Control shows no banner. The one thing you see: Control Web's Sessions shows "Questions can now wait up to 8 hours. In COS Control on your Mac, choose Reinstall hooks." until this Mac's hooks are reinstalled (it does not show while the hooks have a problem, whose line already says Reinstall hooks). **Not now** at the end of the line puts it away in that browser; a browser that cannot store it shows the line again next visit. The lasting on or off is COS Control's setting or `.env`.
|
|
7
|
+
- **If you choose Reinstall hooks** (COS Control on your Mac, Sessions; a newer COS Control offers it with one plain line), a Claude Code question waits up to 8 hours:
|
|
8
|
+
- it shows on Control Web and the glasses at once, even while you sit at the Mac;
|
|
9
|
+
- the Mac's own dialog stays up as before, and the first answer anywhere counts.
|
|
10
|
+
- Only sessions started after the click get it; a session already running keeps the hooks it loaded.
|
|
11
|
+
- **ANY Install or Reinstall hooks turns it on for that Mac, a first install included.** A new Mac whose first step is Install hooks gets the 8 hour wait from day one.
|
|
12
|
+
- **After Reinstall, answering from Control Web tightens, for approvals too:**
|
|
13
|
+
- every answer needs a recent touch on the page;
|
|
14
|
+
- an approval takes a tap, then a confirm;
|
|
15
|
+
- an item no device has been shown yet cannot be answered by its id (`not_shown`).
|
|
16
|
+
These are the 7.0.16 full-hold rules, which until now applied only with a switch on.
|
|
17
|
+
- **How it is decided, per question:** a question gets the long wait only when its own hook asked for more than an hour. Only the question hook Reinstall writes asks for that (8 hours). An older hook asks for about 10 minutes and is treated exactly as before.
|
|
18
|
+
|
|
19
|
+
**Turning it off:**
|
|
20
|
+
- **`COS_QUESTION_FULL_HOLD=0`** in `~/.cos-glasses/.env`, then restart from COS Control and choose Reinstall hooks. `1` turns it on outright, as in 7.0.32.
|
|
21
|
+
- **Or the new setting** a coming COS Control (0.5.394) writes: `PUT /api/session-hooks/question-hold` with `{"mode":"off"}`, `"on"` or `"default"`. It is the Mac app's only: a browser is refused, and Control Web shows the state only.
|
|
22
|
+
- **The `.env` value wins over the setting.** It is what someone set on purpose at the Mac, and every release since 7.0.16 documents it as the off switch, rollback included, so a Settings click never undoes it. While it is set, the route writes nothing and answers `set_by_env`.
|
|
23
|
+
|
|
24
|
+
**Rolling back from 7.0.34: switch off and Reinstall hooks FIRST.**
|
|
25
|
+
- **Back to 7.0.33** with the 8 hour hook still in place: COS Control shows the "needs a refresh" banner (7.0.33 reads unset as off). A question is then held only while a device polls, at most about 10 minutes.
|
|
26
|
+
- **Back to 7.0.31:** it does not accept the 8 hour stamp, so a question is held at most about 120 s until hooks are reinstalled under it.
|
|
27
|
+
- **So, before any rollback:** `COS_QUESTION_FULL_HOLD=0`, restart from COS Control, Reinstall hooks. Then roll back, and Reinstall hooks once more under the older version.
|
|
28
|
+
|
|
29
|
+
**A changed reading:** a value of `COS_QUESTION_FULL_HOLD` that is none of `1`, `true`, `on`, `0`, `false`, `off` (a typo, `yes`, `2`) used to read as OFF. It now reads as the default (on, pending Reinstall hooks). Use `0` to turn it off.
|
|
30
|
+
|
|
31
|
+
**For developers:** `GET /api/session-hooks/status` adds `sessionHooks.questionWait`:
|
|
32
|
+
- `{ mode, source, installedWaitS, waitS, active, offered }`;
|
|
33
|
+
- `offered` is true when Reinstall hooks would turn the long wait on;
|
|
34
|
+
- `state` is unchanged, so an older COS Control shows nothing new.
|
|
35
|
+
|
|
36
|
+
**Not proven yet, said plainly:**
|
|
37
|
+
- The 8 hour wait was measured only on Claude Code 2.1.321 and 2.1.322, overnight, with a stand-in hook. Claude Desktop's engine 2.1.317 to 2.1.320 was proven to 1 hour. Older engines keep the old rule.
|
|
38
|
+
- The real COS server has never held a question for 8 hours end to end.
|
|
39
|
+
- A Mac that sleeps while a question waits is untested.
|
|
40
|
+
- Restarting COS ends any wait in progress; the question stays answerable in the Mac's own dialog.
|
|
41
|
+
- Codex has no question hook, so its questions are unchanged. COS does not hold Cursor's questions.
|
|
42
|
+
- The glasses show "480m left" for a new question until a glasses update says "8h left". The Mac's question card shows no time left.
|
|
43
|
+
|
|
44
|
+
**Tested on this build** (the full suite on its parent 9f3bd8c2; this commit adds only these lines):
|
|
45
|
+
- **The full server suite, alone:** 16,604 passed, 0 failed, type check clean.
|
|
46
|
+
- **The Control Web layout checks in a test browser (WebKit):** the same results as 7.0.33, with nothing new failing.
|
|
47
|
+
- **Not on a real phone, and the real server has not held a question for 8 hours end to end** (as above).
|
|
48
|
+
|
|
49
|
+
**Tests:**
|
|
50
|
+
- `server/lib/question-hold-default.test.ts`: never reinstalled equals switch-off at every layer; reinstalled is the full hold; 0 is off and 1 is on; the environment beats the setting; the route's refusals. The questions route keys are also pinned in `session-questions-queue.test.ts`.
|
|
51
|
+
- Mutation gate `question-hold-default-mutation-gate.mjs`, one mutant per branch. The 7.0.16 full-hold gate's 14 stale targets are repointed: 5 had gone stale with 7.0.32's 8 hour change, 9 with this change. One is retired because 7.0.32 shipped its mutant on purpose.
|
|
52
|
+
|
|
53
|
+
## 7.0.33 - Hotfix: a tap where Ask was could sign out every browser; plus Anand's second round of Control Web fixes
|
|
54
|
+
|
|
55
|
+
2026-11-13. 7.0.32 plus a hotfix and Control Web fixes from Anand's second review. 7.0.32 never reached npm, so anyone updating from 7.0.31 also gets everything in 7.0.32 (listed below again).
|
|
56
|
+
|
|
57
|
+
**The 8-hour question wait is still OFF** unless `COS_QUESTION_FULL_HOLD` is switched on, as in 7.0.32. With it off, a question waits about 10 minutes. Approvals wait 1 hour only with `COS_APPROVAL_FULL_HOLD` on.
|
|
58
|
+
|
|
59
|
+
**The hotfix (7.0.31 and 7.0.32 have this fault).** On a phone, the top bar slides away as you scroll a list. Its strip then showed the page, so a tap meant for the bar landed on whatever was under it. On Settings that could be "Sign out every browser": two taps where Ask had been armed it, then signed out every browser. Reproduced with real taps at a 390 x 844 phone and at the folded Duo, against the published 7.0.31.
|
|
60
|
+
|
|
61
|
+
**On for everyone, with no switch:**
|
|
62
|
+
- **No tap falls through a hidden top bar.** While the bar is away, or for 320 ms while it slides back (also after a page change), the first tap in its strip only brings it back. The strip is the bar's own width, so the folded Duo's rail beside it still takes taps.
|
|
63
|
+
- **Two-tap buttons expire.** Sign out every browser and Settings' Release stay armed for 5 seconds and the second tap counts only after 400 ms, the rule Today's and the question cards' buttons already use. A tap after 5 seconds arms again, even if the phone paused the page. Memories' two-tap buttons use the same 5 seconds (they used 6).
|
|
64
|
+
- **The folded Duo, reading a message:** the top bar stays while you read and shows Back with the name of where it goes ("‹ Today"). The rail's Back stays too.
|
|
65
|
+
- **A time under each message:** "You · 12:20 PM" under your question, and the answer's time under COS's answer. The server adds `queryTimestamp` (when the question was asked) to each message from `GET /api/sessions/today/all-messages` and the archive. With a server older than 7.0.33, only the answer has a time.
|
|
66
|
+
- **Sessions, Usage and COS cost:** the three tabs sit in the same place on all three, and Usage's and COS cost's heading says Usage and COS cost, as the top bar does (it said Sessions).
|
|
67
|
+
- **Today says each thing once.**
|
|
68
|
+
- A source that could not be read is named once, in the sentence. Needs you does not say it again.
|
|
69
|
+
- More than two sources unread reads "Your Mac is not answering, so this may not be everything." in the sentence.
|
|
70
|
+
- Needs you steps aside when it has nothing to add. Under a model summary it still says "Nothing is waiting on you."
|
|
71
|
+
- The Running card steps aside when nothing runs or the session list could not be read, and comes back when something runs.
|
|
72
|
+
- **Ask:** when the words name a place, or its first three letters or more, Go to comes first ("usage" puts Go to Usage first).
|
|
73
|
+
- **No machine codes on screen.** A server answer that is only a code now reads:
|
|
74
|
+
- `cos_pipeline_not_configured`, `pipeline_missing`: "This part of COS is not set up on this Mac, so there is nothing to show here. Nothing is broken." On Memories the box says "Not set up on this Mac.", with no Try again.
|
|
75
|
+
- `bridge_missing`: "COS on your Mac is set to use a scripts folder it cannot run, so this cannot be read. Check the COS_SCRIPTS_DIR setting on your Mac, then restart COS from the COS Control menu."
|
|
76
|
+
- `task_runtime_unavailable`: "Work is not set up on this Mac yet. Open COS Control on your Mac to finish setting it up."
|
|
77
|
+
- `unknown_command`: "COS on your Mac is older than this page. Update it from COS Control on your Mac."
|
|
78
|
+
- `bridge_error`: "COS on your Mac could not read this just now. Try again in a moment. If it keeps happening, restart COS from the COS Control menu on your Mac."
|
|
79
|
+
- any other code: "COS on your Mac could not do that just now. Try again in a moment. If it keeps happening, restart COS from the COS Control menu on your Mac."
|
|
80
|
+
- Memories: "Events in 90 days could not be counted" no longer shows when the list below already says it did not load.
|
|
81
|
+
- Sessions, hooks not installed: "COS cannot yet tell when a Claude Code session is working or waiting on you. In COS Control on your Mac, choose Install hooks."
|
|
82
|
+
- Sessions, hooks out of date: "COS’s link to Claude Code needs a refresh, so working and waiting may be out of date. In COS Control on your Mac, choose Reinstall hooks."
|
|
83
|
+
- Sessions, server too old: "Whether a session is working or waiting needs a newer COS on your Mac. Update it from COS Control on your Mac."
|
|
84
|
+
- **No ⌘K hint on a touch screen** with no mouse (the open Duo, an iPad on its own). A desktop browser keeps it.
|
|
85
|
+
- **The open Duo and wider screens: one way to get around.** Home and Settings left the bar at the top; the rail has Today and Settings.
|
|
86
|
+
- **The trail (how you got here), the same rule at every size:**
|
|
87
|
+
- a place picked from the rail, the pane menu or Ask's Go to starts a new trail;
|
|
88
|
+
- steps are named by what they show, a meeting or session by its title and a message by "Message #N", cut to 28 characters with "…"; the kind ("Meeting") shows only until the title loads; Naming voices keeps that name;
|
|
89
|
+
- from 600 px the bar shows that trail ("Messages / Message #2"), no longer "COS Control › Messages › …".
|
|
90
|
+
- **The pet: three choices** in Settings > Pet: On Today (the default; nobody's Today changes unless they pick), In the top bar, and Off.
|
|
91
|
+
- In the top bar, on a phone: a small round pet beside Ask on every page, keeping its own animation. A tap opens a card with the pet, what it is doing, "I’ll wave when an agent starts." when nothing runs, Change pet and Hide.
|
|
92
|
+
- Hide turns the pet off and says "Pet hidden. Turn it back on in Settings, Pet." The card never covers a banner's button.
|
|
93
|
+
- On the folded and the open Duo the pet stays on Today for now.
|
|
94
|
+
- The Mac's own pet is not changed.
|
|
95
|
+
|
|
96
|
+
**From 7.0.32, also on for everyone (never on npm before this):**
|
|
97
|
+
- the waiting-questions route's `queue` and `queueCount`;
|
|
98
|
+
- a question asked at the end of a turn survives a server restart for its full 8 hours;
|
|
99
|
+
- an answer records which app gave it;
|
|
100
|
+
- the server keeps up to 400 session titles in memory;
|
|
101
|
+
- one new line in the session hook script;
|
|
102
|
+
- the glasses pair by the typed code alone, so no server message says scan (two pairing messages changed);
|
|
103
|
+
- the browser sign-in box takes the glasses' 14-character code;
|
|
104
|
+
- a waiting question's time left reads hours and minutes past an hour.
|
|
105
|
+
|
|
106
|
+
**Tested on this build** (the full suite on its parent 837fb6f0; this commit adds only these lines):
|
|
107
|
+
- **The full server suite, alone:** 16,574 passed, 0 failed, type check clean.
|
|
108
|
+
- **The tap-through, with real taps in a test browser (WebKit):** two taps where Ask was signed out every browser on the published 7.0.31 (a 390 x 844 phone and the folded Duo); on this build 15 of 15 checks pass, and a tap on the folded Duo's rail still lands.
|
|
109
|
+
- **The Control Web layout checks against 7.0.32:** the same results as 7.0.32 (16 in the narrow-and-folded matrix and one short-page check fail as in 7.0.30). Changed on purpose: the folded Duo reader draws Back in its top bar, the bar from 600 px has no Home or Settings, and the trail names steps by title.
|
|
110
|
+
- **Not on a real phone.**
|
|
111
|
+
|
|
112
|
+
**Not in this release:** Work's Filter button and the one refresh control with pull to refresh (both wait for Miles's pick, for 7.0.34), and the pet's place on the Duo.
|
|
113
|
+
|
|
114
|
+
### If you roll back
|
|
115
|
+
|
|
116
|
+
- To 7.0.32: taps can fall through the hidden top bar again, the two-tap buttons stay armed with no limit, messages have one time, Today repeats a failure, Ask lists Go to last, codes can show on screen, ⌘K shows on touch, the bar has Home and Settings, the trail names steps by kind, and the pet has only on and off (a pet set to the top bar shows on Today).
|
|
117
|
+
- To 7.0.31: also as in 7.0.32's rollback below.
|
|
118
|
+
|
|
119
|
+
## 7.0.32 - Questions can wait 8 hours (off unless switched on), one queue of waiting questions, code-only glasses pairing words, and the sign-in box takes the glasses code
|
|
120
|
+
|
|
121
|
+
2026-11-13. 7.0.31 plus three pieces: X1's question wait, W1's pairing words and sign-in field, and one Control Web label.
|
|
122
|
+
|
|
123
|
+
**The 8-hour question wait is OFF.** With it off, a question waits about 10 minutes, as before. It turns on only when `COS_QUESTION_FULL_HOLD` is switched on. On 11/12 at 20:17 neither switch was set anywhere on Miles's Mac.
|
|
124
|
+
- **To turn it on:** set `COS_QUESTION_FULL_HOLD=1` in `~/.cos-glasses/.env`, restart the server, then choose **Reinstall hooks**. The installed hook keeps its old wait, and shows the Reinstall banner, until then.
|
|
125
|
+
- **With it on:**
|
|
126
|
+
- a question Claude asks waits up to 8 hours, and can be answered from the web, the glasses or the Mac;
|
|
127
|
+
- it shows on all of them at once;
|
|
128
|
+
- the first answer wins, and it stays everywhere until answered.
|
|
129
|
+
- **Approvals** wait 1 hour only with `COS_APPROVAL_FULL_HOLD` on (also off), otherwise about 10 minutes.
|
|
130
|
+
The 8 hours was measured on a real engine: a hook held 7 h 55 m and its answer was applied.
|
|
131
|
+
|
|
132
|
+
**On for everyone, with no switch:**
|
|
133
|
+
- **The waiting-questions route gains a queue.** `GET /api/session-questions` adds `queue` and `queueCount`, one list of every question or approval waiting anywhere, with answered ones for two minutes. No current client reads them yet.
|
|
134
|
+
- A waiting question's entry carries its first 80 characters only, as sessions already do (off with `COS_WAITING_QUESTION_WORDS=0`).
|
|
135
|
+
- Approvals waiting only at the Mac, and answered entries, carry no words.
|
|
136
|
+
- **A question asked at the end of a turn survives a server restart for its full 8 hours.** Before, one asked 6 to 8 hours before a restart was lost.
|
|
137
|
+
- **An answer records which app gave it** (web, glasses, phone or Mac), from the app's own label. It is a label for the queue, not proof of who answered.
|
|
138
|
+
- **The server keeps the titles of up to 400 sessions in memory**, filled by the session list, so the queue can name them.
|
|
139
|
+
- **The session hook script carries one new line.** A hook installed by 7.0.31 still counts as installed. Nothing asks to reinstall it; it matters only after a Reinstall.
|
|
140
|
+
- **The glasses pair by the code alone, so no server message says scan.** Signing a browser in by QR is unchanged, and Control Web, its Settings and the sign-in page already had no such line. Two messages the glasses app shows changed:
|
|
141
|
+
- a pairing request that was not understood: "That pairing request was not understood. Scan the code again." becomes "That pairing request was not understood. Check the code in COS Control and type it again." The parity copy `docs/pairing-fixture.json` changes with it.
|
|
142
|
+
- a request without the pairing token: "Paste the pairing token from COS Control, or scan its code with COS Glasses 6.10.621 or newer." becomes "Paste the pairing token from COS Control, or update COS Glasses and type the pairing code COS Control shows." Clients act on its reason, not its words.
|
|
143
|
+
- **The browser sign-in box takes the glasses' full 14-character code** (17 keys with its dashes; it took 9 before). Typed with or without dashes, it says it is the glasses code. It is recognised by its shape, never looked up, and nothing is used up. A real 8-character sign-in code works as before.
|
|
144
|
+
- **Control Web: a waiting question's time left reads hours and minutes past an hour**, for example "7 h 59 m left", where an 8-hour question would have read "480 min left".
|
|
145
|
+
|
|
146
|
+
**Not in this release (X1's other two asks for Control Web):**
|
|
147
|
+
- the Today count, the Sessions rail and the pet reading the shared queue;
|
|
148
|
+
- "Answered on <app>" on a card answered elsewhere.
|
|
149
|
+
Both are for a later release, before the 8-hour switch is ever turned on.
|
|
150
|
+
|
|
151
|
+
**Tested on this build** (the full suite on its parent 90453233; this commit adds only these lines):
|
|
152
|
+
- **The full server suite, alone:** 16,543 passed, 0 failed, type check clean.
|
|
153
|
+
- **The Control Web layout checks, in a test browser (WebKit) against 7.0.31:** every check passed except 16 in the narrow-and-folded matrix and one known short-page check, all of which fail the same way in 7.0.30.
|
|
154
|
+
- **The sign-in box:** 17 typed keys reach the server (7.0.31 kept 9).
|
|
155
|
+
- **Not on a real phone.**
|
|
156
|
+
|
|
157
|
+
### If you roll back
|
|
158
|
+
|
|
159
|
+
- To 7.0.31: no queue fields, a turn-end question older than 6 hours is lost on a restart, the sign-in box takes 9 characters, the two pairing messages say scan again, and the time left reads in minutes. A question waits about 10 minutes, as it did in 7.0.32 unless the switch was on; a hook reinstalled with the switch on keeps its longer wait until it is reinstalled again.
|
|
160
|
+
- Further back: as in 7.0.31 below.
|
|
161
|
+
|
|
162
|
+
## 7.0.31 - Control Web: a message's text no longer runs under and below Reply on a wide screen
|
|
163
|
+
|
|
164
|
+
2026-11-12. A hotfix to 7.0.30, nothing else in it.
|
|
165
|
+
|
|
166
|
+
- **7.0.30 shipped this fault.** On a wide screen (the open Duo, an iPad, a desktop browser), a message's text could run behind and below the pinned Reply box. The top bar was gone, and the left rail started partway down its list and ended partway down the screen. Miles saw it on the open Duo at 17:25 and could not make it happen again. It came from 7.0.30's Reply staying at the bottom of the screen.
|
|
167
|
+
- **The cause.** The message reader was the whole page. Its Reply box was pinned to Safari's idea of the screen, and the rail was as tall as that idea. On his Duo, typing a reply moved Safari's view about 409 px down the page, as a keyboard does, so the field stays in sight. After the keyboard closed, he folded and unfolded the Duo five times. Safari then reported its visible area as 0 px tall, then as a large negative number, with its top still about 409 px down. The view stayed shifted, and Reply and the rail stopped about halfway down the screen, with the rest of the message painted below them. It needs that keyboard-then-fold sequence, which is why it came and went. No part of the page was too tall for its box.
|
|
168
|
+
- **The fix.** From 600 px wide, an open message is now one screen, the way Sessions already is. The rail and the area under the top bar each scroll by themselves, and the page itself no longer scrolls. The sign-in notice, a question card and the message still scroll together, as in 7.0.30, and Reply stays at the bottom of that area. At the end of a message, the Reply section sits just above the Reply box, with nothing under it. With the keyboard up, the box sits above the keyboard as before. The screen's height comes from the page's own measurement, which falls back to the window's height when Safari's reading is broken.
|
|
169
|
+
- **What 7.0.31 does and does not fix.**
|
|
170
|
+
- **Fixed:** it stops the message showing behind and below Reply.
|
|
171
|
+
- **Not fixed:** Safari leaving the page pushed down after the keyboard and a fold. If that happens again, the app can sit shifted, with a blank band at the bottom of the screen and the top bar out of sight. It returns to the full screen when Safari's view moves back, for example on the next fold, rotation or keyboard. In a browser test, the page refilled the screen after a resize, with no reload.
|
|
172
|
+
- **Not done:** the page does not follow Safari's reported position. On his Duo that reading was 40 to 85 px off while the height read 0.
|
|
173
|
+
- **What's next:** we now record Safari's readings on every message opened, every fold and every keyboard close, to find out how often it happens.
|
|
174
|
+
- **Seen once in the readings we have:** today, from 07:42, the only stranded push came after a keyboard in the message reader. After five keyboard uses in Sessions and one in the COS chat, which were already one screen, every following reading was back at the top, through folds that broke the height readings the same way. One day of readings proves nothing either way.
|
|
175
|
+
- **On the open Duo with a question card showing**, a short message's Reply box starts just below the bottom of the screen, as in 7.0.30. The page stays one screen, and scrolling the message brings Reply up.
|
|
176
|
+
- **Folding or unfolding while reading keeps your place.** A fold or an unfold moves the message between the page's own scroll (narrow) and the framed area (wide). On its own the new view would open at the top, so the line you were reading is put back at the top, once, after the fold. It waits until your finger is off the screen and the page is still. If you scroll first, your scroll wins and it does nothing. Nothing else moves the text: not the keyboard, not Safari's bars, not a question card or notice appearing.
|
|
177
|
+
- **Phones and the folded Duo otherwise unchanged.** Under 600 px, with no fold, the message reader works as in 7.0.30, with the side rail on the folded Duo.
|
|
178
|
+
- **The Reply section stays.** The "Reply. Continues this conversation" block above the box is where replies sent from that message appear. It is not a second Reply box.
|
|
179
|
+
- **The rest of the app in the same Safari state** (Today, the Messages list, Meetings, Speakers, Work and Settings, at 600 px and wider): no pinned part has text under it, but the rail can end partway down with the page continuing below it. Sessions already stays inside one screen. Those panes are not changed in this hotfix.
|
|
180
|
+
- **New numbers in the page's own layout reports**, numbers only, to the Mac's diagnostics log:
|
|
181
|
+
- the height of 100dvh at each fold;
|
|
182
|
+
- for every message opened, at any width: the visible area, the message's box, the scrolling area's height and content height, where Reply sits, the page's height and scroll, and 100dvh. The page already sends the window's size and Safari's visible area with each report, so a shifted view shows as its top.
|
|
183
|
+
The 17:25 frame could not be measured; the next one will be.
|
|
184
|
+
- **Tested in a browser (WebKit)**, in light and dark:
|
|
185
|
+
- **His two Safari states, recreated:** the view shifted 409 px, and a window 430 px tall drawn 787 px tall. 7.0.30 paints text below Reply in both; this build paints none.
|
|
186
|
+
- **A long message at seven sizes:** a tall iPhone, the folded Duo at 85% zoom, 1000x703, the open Duo at 1119x787 and 951x539, an iPad at 1180x820, and a desktop at 1440x900. 7.0.30 fails 20 of 48 of these checks; this build passes all 48.
|
|
187
|
+
- **The Sessions check list on the message reader:**
|
|
188
|
+
- typing with the keyboard up, with two files attached;
|
|
189
|
+
- folding and unfolding without a reload;
|
|
190
|
+
- Back while reading, and the how-you-got-here line on a phone;
|
|
191
|
+
- the browser's find, and selecting text;
|
|
192
|
+
- the question card;
|
|
193
|
+
- a short message and a 14,000 px message;
|
|
194
|
+
- the scroll position kept when the page refreshes the same message.
|
|
195
|
+
|
|
196
|
+
These ran at 951x539 and 1119x787 at 85%, 382x557, 449x797, 1180x820 and 1440x900. The keyboard was simulated by shrinking the window, the way an Android keyboard does; an iPhone keyboard shifts the view instead, which was not simulated. A reply arriving while you read was not driven.
|
|
197
|
+
- **Not yet:** a real phone. This counts as done only when Miles has looked at it on the Duo.
|
|
198
|
+
|
|
199
|
+
**Default-on for every install:** yes, with no switch, two things: on screens 600 px and wider, an open message is one screen and the area under the top bar scrolls instead of the page; and an open message keeps your reading place across a fold or an unfold. Nothing else changes under 600 px.
|
|
200
|
+
|
|
201
|
+
### If you roll back
|
|
202
|
+
|
|
203
|
+
- To 7.0.30: an open message is the whole page again, and after the keyboard and folds its Reply box and the rail can stop partway down the screen with the message below them. A fold or an unfold moves you a few paragraphs back.
|
|
204
|
+
- Further back: as in 7.0.30 below.
|
|
205
|
+
|
|
1
206
|
## 7.0.30 - Control Web: Back goes the way you came, a phone shows how you got here, a message shows who said what, Reply stays at the bottom, Meetings opens on today, and clearer copy buttons
|
|
2
207
|
|
|
3
208
|
2026-11-12. 7.0.29 plus eight Control Web changes from a tester's phone feedback, one clearer sign-in error, and phone-approval changes that stay off unless switched on.
|
|
@@ -23,6 +228,8 @@ Server:
|
|
|
23
228
|
|
|
24
229
|
**Default-on for every install:** yes, all eight, with no switch, at every screen size (the how-you-got-here line below 600 px only). The sign-in line is on for everyone. The phone-approval items do nothing while the switch is off, which is the default everywhere.
|
|
25
230
|
|
|
231
|
+
**Known fault, fixed in 7.0.31:** on a wide screen, after the keyboard and then folds on the Duo, a message's text could run behind and below the pinned Reply box. 7.0.31 stops that. Safari can still leave the page pushed down after the keyboard and a fold; 7.0.31 does not fix that and records it when it happens.
|
|
232
|
+
|
|
26
233
|
### If you roll back
|
|
27
234
|
|
|
28
235
|
- To 7.0.29: Back goes to a fixed parent again, the top bar tucks away on a reader, the message reader's Reply is at the end of the page, the copy buttons say Summary and Full, Meetings opens on today only when today has meetings, Settings says iPhone, a message shows the YOU and COS cards, the how-you-got-here line is gone, and an expired Claude sign-in reads "provider process exited" again. Nothing is stored.
|
|
@@ -150,11 +150,12 @@ case "$(head -c 1 "$T.in")" in '{') ;; *) EVENT=rejected ;; esac
|
|
|
150
150
|
# 6.60.0: the installer passes the PermissionRequest block's own timeout as $2. Claude Code
|
|
151
151
|
# stops this hook at that many seconds, so curl below gives up 5 s before it, and the
|
|
152
152
|
# request carries it (`hookWaitS`) so the server never holds it longer. No usable $2 (an
|
|
153
|
-
# earlier block) is the earlier wait, 130, with no stamp.
|
|
153
|
+
# earlier block) is the earlier wait, 130, with no stamp. X1: up to FULL_HOLD_QUESTION_WAIT_S (28800, 8 h;
|
|
154
|
+
# server/lib/full-hold.ts), the full hold's question block; a test pins this line to that constant.
|
|
154
155
|
WAIT=130; STAMP_WAIT=''
|
|
155
156
|
if [ "$EVENT" = PermissionRequest ]; then
|
|
156
157
|
case "${2:-}" in ''|0*|*[!0-9]*) ;; *)
|
|
157
|
-
if [ ${#2} -le
|
|
158
|
+
if [ ${#2} -le 5 ] && [ "$2" -ge 20 ] && [ "$2" -le 28800 ]; then WAIT=$2; STAMP_WAIT=",\"hookWaitS\":$2"; fi ;;
|
|
158
159
|
esac
|
|
159
160
|
fi
|
|
160
161
|
# 6.71.x: only a correctly read question from the new script skips the desk fast path.
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"draining": 503
|
|
66
66
|
},
|
|
67
67
|
"reasonMessages": {
|
|
68
|
-
"bad_request": "That pairing request was not understood.
|
|
68
|
+
"bad_request": "That pairing request was not understood. Check the code in COS Control and type it again.",
|
|
69
69
|
"not_loopback": "Pairing codes are made only by COS Control on this Mac.",
|
|
70
70
|
"not_allowed_network": "Connect over Tailscale, or turn on \"Allow pairing on this Wi-Fi\" in COS Control.",
|
|
71
71
|
"denied": "Pairing was denied on the Mac.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.34",
|
|
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": {
|
|
@@ -155,6 +155,8 @@
|
|
|
155
155
|
"!server/scripts/compact-session-log.ts",
|
|
156
156
|
"!server/scripts/full-hold-mutation-gate.mjs",
|
|
157
157
|
"!server/scripts/turn-end-ask-mutation-gate.mjs",
|
|
158
|
+
"!server/scripts/question-8h-mutation-gate.mjs",
|
|
159
|
+
"!server/scripts/question-hold-default-mutation-gate.mjs",
|
|
158
160
|
"docs/web-key-verifier-contract.md"
|
|
159
161
|
],
|
|
160
162
|
"author": "COS Contributors",
|
package/server/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { createPermissionBrokerHookRouter, createSessionQuestionsRouter } from './routes/permission-broker.js'
|
|
59
59
|
// X1: the full hold's switches and the engine gate's registry read (kept on their own lines, apart from other lanes' imports).
|
|
60
60
|
import { fullHoldSwitches } from './lib/full-hold.js'
|
|
61
|
+
import { readQuestionHoldSetting } from './lib/question-hold-setting.js'
|
|
61
62
|
import { engineForHookProcess, registryStatusForPid } from './lib/full-hold-engine.js'
|
|
62
63
|
import { lastHoldQuestionsPollAt, lastLegacyQuestionsPollAt, lastQuestionsPollAt, presenceAgeMs, webPresenceAgeMs } from './lib/client-liveness.js'
|
|
63
64
|
import {
|
|
@@ -377,8 +378,9 @@ const permissionBroker = new PermissionBroker({
|
|
|
377
378
|
return !!signal && !signal.ended && signal.waiting?.fingerprint === fingerprint
|
|
378
379
|
},
|
|
379
380
|
signals: brokerSignalSink(sessionSignalStore),
|
|
380
|
-
// X1: the full hold (lib/full-hold.ts).
|
|
381
|
-
|
|
381
|
+
// X1: the full hold (lib/full-hold.ts). Approvals default OFF. 7.0.34: questions default ON, pending per request
|
|
382
|
+
// (a long stamp from a reinstalled hook); `COS_QUESTION_FULL_HOLD` wins, then the Settings row's file.
|
|
383
|
+
fullHold: () => fullHoldSwitches(process.env, readQuestionHoldSetting()),
|
|
382
384
|
engineFor: (sessionId, ppid) => engineForHookProcess(sessionId, ppid),
|
|
383
385
|
// X1 round 3: the asking engine's own status, read each tick for full-hold items only.
|
|
384
386
|
engineStatus: (sessionId, pid) => registryStatusForPid(pid, sessionId),
|
package/server/lib/api-auth.ts
CHANGED
|
@@ -134,8 +134,9 @@ export function requireApiToken(apiToken: string, options: RequireApiTokenOption
|
|
|
134
134
|
error: 'unauthorized',
|
|
135
135
|
// Stable on purpose: clients key off this value, not the message.
|
|
136
136
|
reason: 'pairing_token_rejected',
|
|
137
|
-
// Older COS Glasses builds
|
|
138
|
-
|
|
137
|
+
// Older COS Glasses builds show this message word for word. 7.0.32 (Miles 11/13 06:34): the glasses pair by
|
|
138
|
+
// the typed code only, so it no longer says scan.
|
|
139
|
+
message: 'Paste the pairing token from COS Control, or update COS Glasses and type the pairing code COS Control shows.',
|
|
139
140
|
})
|
|
140
141
|
}
|
|
141
142
|
}
|
package/server/lib/archive.ts
CHANGED
|
@@ -510,11 +510,11 @@ export function getArchiveChatMessages(
|
|
|
510
510
|
* (sessionId, timestamp) instead of bare timestamp (collision-prone). */
|
|
511
511
|
export function getArchiveDayMessages(
|
|
512
512
|
date: string,
|
|
513
|
-
): Array<{ query: string; text: string; timestamp: number; chatIndex: number; sessionId: string; no?: number; globalMsgNum?: number; messageEra?: string; modelPreference?: string; origin?: 'routine' | 'task'; originId?: string; attachments?: MediaAttachmentRef[] }> {
|
|
513
|
+
): Array<{ query: string; text: string; timestamp: number; queryTimestamp?: number; chatIndex: number; sessionId: string; no?: number; globalMsgNum?: number; messageEra?: string; modelPreference?: string; origin?: 'routine' | 'task'; originId?: string; attachments?: MediaAttachmentRef[] }> {
|
|
514
514
|
const archive = loadArchive(date)
|
|
515
515
|
if (!archive) return []
|
|
516
516
|
|
|
517
|
-
const messages: Array<{ query: string; text: string; timestamp: number; chatIndex: number; sessionId: string; no?: number; globalMsgNum?: number; messageEra?: string; modelPreference?: string; origin?: 'routine' | 'task'; originId?: string; attachments?: MediaAttachmentRef[] }> = []
|
|
517
|
+
const messages: Array<{ query: string; text: string; timestamp: number; queryTimestamp?: number; chatIndex: number; sessionId: string; no?: number; globalMsgNum?: number; messageEra?: string; modelPreference?: string; origin?: 'routine' | 'task'; originId?: string; attachments?: MediaAttachmentRef[] }> = []
|
|
518
518
|
for (const chat of archive.chats) {
|
|
519
519
|
for (let i = 0; i < chat.exchanges.length; i++) {
|
|
520
520
|
const ex = chat.exchanges[i]
|
|
@@ -531,6 +531,8 @@ export function getArchiveDayMessages(
|
|
|
531
531
|
query: ex.content,
|
|
532
532
|
text: next.content,
|
|
533
533
|
timestamp: next.timestamp,
|
|
534
|
+
// 7.0.33 A2: the question's own time, as in the live view (routes/sessions.ts).
|
|
535
|
+
...(Number.isFinite(ex.timestamp) ? { queryTimestamp: ex.timestamp } : {}),
|
|
534
536
|
chatIndex: chat.id,
|
|
535
537
|
sessionId: chat.sessionId,
|
|
536
538
|
...(globalMsgNum != null ? { no: globalMsgNum, globalMsgNum } : {}),
|
|
@@ -24,7 +24,8 @@ import { homedir } from 'node:os'
|
|
|
24
24
|
import { dirname, join, resolve } from 'node:path'
|
|
25
25
|
import { fileURLToPath } from 'node:url'
|
|
26
26
|
import { atomicWriteFileSync } from './atomic-fs.js'
|
|
27
|
-
import { FULL_HOLD_HOOK_WAIT_S, NON_QUESTION_MATCHER, QUESTION_MATCHER,
|
|
27
|
+
import { FULL_HOLD_HOOK_WAIT_S, FULL_HOLD_QUESTION_WAIT_PRIOR_S, FULL_HOLD_QUESTION_WAIT_S, NON_QUESTION_MATCHER, QUESTION_MATCHER, approvalFullHoldEnabled, defaultHoldTakesStamp, questionHoldMode, type QuestionHoldMode, type QuestionHoldSetting, type QuestionHoldSource } from './full-hold.js'
|
|
28
|
+
import { questionHoldSettingPath, readQuestionHoldSetting } from './question-hold-setting.js'
|
|
28
29
|
|
|
29
30
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
30
31
|
|
|
@@ -193,19 +194,68 @@ function priorWaitBlock(scriptPath: string, sub: HookSubscription, paths: HookPa
|
|
|
193
194
|
* `COS_APPROVAL_FULL_HOLD` both off (the default) it is the ONE block every release since 6.60.0
|
|
194
195
|
* writes, byte for byte (`split: false`). With either on it is TWO blocks of ours, split by matcher
|
|
195
196
|
* (canary C9, both engines): `AskUserQuestion` waits `questionWaitS`, everything else `otherWaitS`;
|
|
196
|
-
* a
|
|
197
|
+
* a question whose switch is on waits FULL_HOLD_QUESTION_WAIT_S (8 h), an approval FULL_HOLD_HOOK_WAIT_S (1 h),
|
|
198
|
+
* and a kind whose switch is off keeps 630. Each block passes its
|
|
197
199
|
* own wait to the script, so the stamp a request carries is its own block's. Reinstall hooks is what
|
|
198
200
|
* writes it: until then the file reads `drift` (`priorWaitOnly`, still halt-ready).
|
|
199
201
|
*/
|
|
200
202
|
export interface PermissionHookPlan { split: boolean; questionWaitS: number; otherWaitS: number }
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
/**
|
|
205
|
+
* X1, 7.0.34: under the DEFAULT question hold (`questionHoldMode` `default`) the plan depends on who asks.
|
|
206
|
+
* - Reinstall hooks (`forInstall`): the split block with the 8 h question wait. That click is the opt-in.
|
|
207
|
+
* - The status: the split plan only once the file already carries a question block longer than
|
|
208
|
+
* DEFAULT_HOLD_MIN_STAMP_S (`installedQuestionWaitS`); before that, the one unsplit block, so a Mac that never
|
|
209
|
+
* clicks reads `installed` with no banner and the 7.0.31 script stays current, exactly as 7.0.33.
|
|
210
|
+
* Explicit on and off (env or setting) are the same for both, as before.
|
|
211
|
+
*/
|
|
212
|
+
export interface PermissionHookPlanOptions {
|
|
213
|
+
/** The server-side setting; absent reads the file (`lib/question-hold-setting.ts`). */
|
|
214
|
+
setting?: QuestionHoldSetting | null
|
|
215
|
+
/** The AskUserQuestion block's timeout in the settings file now (status only). */
|
|
216
|
+
installedQuestionWaitS?: number | null
|
|
217
|
+
/** Reinstall hooks is asking: under the default, write the long question block. */
|
|
218
|
+
forInstall?: boolean
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function permissionHookPlan(env: NodeJS.ProcessEnv = process.env, options: PermissionHookPlanOptions = {}): PermissionHookPlan {
|
|
222
|
+
const setting = options.setting === undefined ? readQuestionHoldSetting() : options.setting
|
|
223
|
+
const mode = questionHoldMode(env, setting).mode
|
|
224
|
+
const questions = mode === 'on' || (mode === 'default' && (options.forInstall === true || defaultHoldTakesStamp(options.installedQuestionWaitS)))
|
|
225
|
+
const approvals = approvalFullHoldEnabled(env)
|
|
226
|
+
if (!questions && !approvals) return { split: false, questionWaitS: PERMISSION_HOOK_TIMEOUT_S, otherWaitS: PERMISSION_HOOK_TIMEOUT_S }
|
|
205
227
|
return {
|
|
206
228
|
split: true,
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
// X1 (Miles 11/12 17:42): a held question waits 8 hours; approvals keep the 7.0.16 hour.
|
|
230
|
+
questionWaitS: questions ? FULL_HOLD_QUESTION_WAIT_S : PERMISSION_HOOK_TIMEOUT_S,
|
|
231
|
+
otherWaitS: approvals ? FULL_HOLD_HOOK_WAIT_S : PERMISSION_HOOK_TIMEOUT_S,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* X1, 7.0.34: what `GET /api/session-hooks/status` adds as `sessionHooks.questionWait` (additive: `state` is
|
|
237
|
+
* untouched, so a Control that predates it shows exactly what it showed). `offered`: the default is on and this
|
|
238
|
+
* Mac's hooks do not carry the long question block yet, so Reinstall hooks would turn it on. `active`: the hooks
|
|
239
|
+
* carry it and the hold is not off, so a question from a session started since is held up to `waitS`.
|
|
240
|
+
*/
|
|
241
|
+
export interface QuestionWaitStatus {
|
|
242
|
+
mode: QuestionHoldMode
|
|
243
|
+
source: QuestionHoldSource
|
|
244
|
+
installedWaitS: number | null
|
|
245
|
+
waitS: number
|
|
246
|
+
active: boolean
|
|
247
|
+
offered: boolean
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function questionWaitStatus(env: NodeJS.ProcessEnv, setting: QuestionHoldSetting | null, installedQuestionWaitS: number | null): QuestionWaitStatus {
|
|
251
|
+
const { mode, source } = questionHoldMode(env, setting)
|
|
252
|
+
const long = defaultHoldTakesStamp(installedQuestionWaitS)
|
|
253
|
+
return {
|
|
254
|
+
mode, source,
|
|
255
|
+
installedWaitS: installedQuestionWaitS,
|
|
256
|
+
waitS: FULL_HOLD_QUESTION_WAIT_S,
|
|
257
|
+
active: mode !== 'off' && long,
|
|
258
|
+
offered: mode === 'default' && !long,
|
|
209
259
|
}
|
|
210
260
|
}
|
|
211
261
|
|
|
@@ -235,8 +285,10 @@ function priorWaitVariants(scriptPath: string, sub: HookSubscription, paths: Hoo
|
|
|
235
285
|
if (!prior) return []
|
|
236
286
|
const out: Record<string, unknown>[][] = [[prior]]
|
|
237
287
|
if (plan.split) out.push([canonicalBlock(scriptPath, sub, paths)])
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
// X1: the question block also had 3600 (7.0.16 to 7.0.31) before FULL_HOLD_QUESTION_WAIT_S.
|
|
289
|
+
const questionWaits = [PERMISSION_HOOK_TIMEOUT_S, FULL_HOLD_QUESTION_WAIT_PRIOR_S, FULL_HOLD_QUESTION_WAIT_S]
|
|
290
|
+
const otherWaits = [PERMISSION_HOOK_TIMEOUT_S, FULL_HOLD_HOOK_WAIT_S]
|
|
291
|
+
for (const q of questionWaits) for (const o of otherWaits) {
|
|
240
292
|
if (plan.split && q === plan.questionWaitS && o === plan.otherWaitS) continue
|
|
241
293
|
if (q === PERMISSION_HOOK_TIMEOUT_S && o === PERMISSION_HOOK_TIMEOUT_S) continue
|
|
242
294
|
out.push(splitPermissionBlocks(scriptPath, sub, paths, q, o))
|
|
@@ -405,6 +457,8 @@ export interface HookStatus {
|
|
|
405
457
|
*/
|
|
406
458
|
permissionHookTimeouts?: { question: number | null; approval: number | null }
|
|
407
459
|
tokenPresent: boolean
|
|
460
|
+
/** X1, 7.0.34: the question hold for this Mac (`questionWaitStatus`); absent only when the settings could not be read. */
|
|
461
|
+
questionWait?: QuestionWaitStatus
|
|
408
462
|
}
|
|
409
463
|
|
|
410
464
|
function sha256File(path: string): string | null {
|
|
@@ -435,7 +489,7 @@ function hooksDisabled(settings: unknown): boolean {
|
|
|
435
489
|
return !!settings && typeof settings === 'object' && (settings as { disableAllHooks?: unknown }).disableAllHooks === true
|
|
436
490
|
}
|
|
437
491
|
|
|
438
|
-
export function hookStatus(paths: { settingsPath?: string; scriptPath?: string; packageScriptPath?: string; hookPaths?: HookPaths; plan?: PermissionHookPlan } = {}): HookStatus {
|
|
492
|
+
export function hookStatus(paths: { settingsPath?: string; scriptPath?: string; packageScriptPath?: string; hookPaths?: HookPaths; plan?: PermissionHookPlan; env?: NodeJS.ProcessEnv; setting?: QuestionHoldSetting | null } = {}): HookStatus {
|
|
439
493
|
const settingsPath = paths.settingsPath ?? claudeSettingsPath()
|
|
440
494
|
const scriptPath = paths.scriptPath ?? stableHookScriptPath()
|
|
441
495
|
const packageScriptPath = paths.packageScriptPath ?? packagedHookScriptPath()
|
|
@@ -447,7 +501,13 @@ export function hookStatus(paths: { settingsPath?: string; scriptPath?: string;
|
|
|
447
501
|
if (!read.ok) {
|
|
448
502
|
return { state: read.reason, installed: false, settingsPath, scriptPath, scriptSha, packageScriptSha, subscribed: [], missing: HOOK_SUBSCRIPTIONS.map(s => s.event), drifted: [], priorWait: [], priorWaitOnly: false, permissionHookTimeoutS: null, tokenPresent }
|
|
449
503
|
}
|
|
450
|
-
|
|
504
|
+
// X1, 7.0.34: the setting is read once per status; the plan then follows what the file already carries.
|
|
505
|
+
const env = paths.env ?? process.env
|
|
506
|
+
// The setting lives in the same COS home the hooks point at (production: ~/.cos-glasses; a test: its own box).
|
|
507
|
+
const setting = paths.setting === undefined ? readQuestionHoldSetting(questionHoldSettingPath(hookPaths.home)) : paths.setting
|
|
508
|
+
const installedQuestionWaitS = permissionHookTimeoutForToolS(read.settings, 'AskUserQuestion')
|
|
509
|
+
const plan = paths.plan ?? permissionHookPlan(env, { setting, installedQuestionWaitS })
|
|
510
|
+
const events = subscribedEvents(read.settings, scriptPath, hookPaths, plan)
|
|
451
511
|
const permissionWaitS = permissionHookTimeoutS(read.settings)
|
|
452
512
|
const permissionHookTimeouts = {
|
|
453
513
|
question: permissionHookTimeoutForToolS(read.settings, 'AskUserQuestion'),
|
|
@@ -457,12 +517,12 @@ export function hookStatus(paths: { settingsPath?: string; scriptPath?: string;
|
|
|
457
517
|
if (hooksDisabled(read.settings)) state = 'disabled_by_settings'
|
|
458
518
|
else if (events.subscribed.length === 0 && events.drifted.length === 0) state = 'missing'
|
|
459
519
|
else if (events.missing.length > 0 || events.drifted.length > 0) state = 'drift'
|
|
460
|
-
else if (!scriptSha
|
|
520
|
+
else if (!scriptCurrentForPlan(scriptSha, packageScriptSha, plan)) state = 'script_outdated'
|
|
461
521
|
else state = 'installed'
|
|
462
522
|
const priorWaitOnly = state === 'drift' && events.missing.length === 0 && events.drifted.length > 0
|
|
463
523
|
&& events.drifted.every(event => events.priorWait.includes(event))
|
|
464
524
|
const observerOnlyDrift = state === 'drift' && events.missing.length > 0 && events.missing.every(event => event === 'PreCompact') && events.drifted.every(event => events.priorWait.includes(event))
|
|
465
|
-
return { observerOnlyDrift, state, installed: state === 'installed', settingsPath, scriptPath, scriptSha, packageScriptSha, ...events, priorWaitOnly, permissionHookTimeoutS: permissionWaitS, ...(
|
|
525
|
+
return { observerOnlyDrift, state, installed: state === 'installed', settingsPath, scriptPath, scriptSha, packageScriptSha, ...events, priorWaitOnly, permissionHookTimeoutS: permissionWaitS, ...(plan.split || permissionHookTimeouts.question !== permissionHookTimeouts.approval ? { permissionHookTimeouts } : {}), tokenPresent, questionWait: questionWaitStatus(env, setting, installedQuestionWaitS) }
|
|
466
526
|
}
|
|
467
527
|
|
|
468
528
|
/**
|
|
@@ -493,8 +553,31 @@ export const HALT_CAPABLE_PRIOR_SCRIPT_SHAS: readonly string[] = [
|
|
|
493
553
|
'0e1bbb9816c9475436f89f6326d5504eccc9aa6519ff5e466b949d53a73d8439',
|
|
494
554
|
'1877c90c5359571dfc74c6d03395104a73925868147e8a50cd9d7b4226b1b5ca',
|
|
495
555
|
'0fd4f98801333399625701bdc83ed1a5906bbf18c449d0586daa6b8e58ada7f3',
|
|
556
|
+
// X1: the 6.71.7 to 7.0.31 script (WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS below); its halt check is this package's.
|
|
557
|
+
'ee32d7a6b051cc1b9250c0b732fbfe528a59b4930daaf27696830d5803d6687a',
|
|
496
558
|
]
|
|
497
559
|
|
|
560
|
+
/**
|
|
561
|
+
* X1 (8 h question wait): the script 6.71.7 through 7.0.31 shipped. It differs from this package's in ONE line,
|
|
562
|
+
* the longest PermissionRequest wait it accepts (3600 then, FULL_HOLD_QUESTION_WAIT_S now), and only a question
|
|
563
|
+
* block written with the full hold on ever passes more than 3600. So while the plan does not ask for a longer
|
|
564
|
+
* question wait it does everything this package's script does, and is reported current: no Reinstall banner,
|
|
565
|
+
* Cursor and Codex desk cancel unchanged. A Mac that never turns COS_QUESTION_FULL_HOLD on sees no change. With
|
|
566
|
+
* the switch on it reads `script_outdated` until Reinstall hooks copies this package's script (which that switch
|
|
567
|
+
* needs anyway for the split block). Pinned against `__fixtures__/cos-session-hook-7.0.31` by a test that diffs
|
|
568
|
+
* it with this package's script line by line.
|
|
569
|
+
*/
|
|
570
|
+
export const WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS: readonly string[] = [
|
|
571
|
+
'ee32d7a6b051cc1b9250c0b732fbfe528a59b4930daaf27696830d5803d6687a',
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
/** Whether the installed script is current for this plan (see WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS). */
|
|
575
|
+
export function scriptCurrentForPlan(scriptSha: string | null, packageScriptSha: string | null, plan: Pick<PermissionHookPlan, 'questionWaitS'>): boolean {
|
|
576
|
+
if (!scriptSha) return false
|
|
577
|
+
if (!packageScriptSha || scriptSha === packageScriptSha) return true
|
|
578
|
+
return plan.questionWaitS <= FULL_HOLD_QUESTION_WAIT_PRIOR_S && WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS.includes(scriptSha)
|
|
579
|
+
}
|
|
580
|
+
|
|
498
581
|
/**
|
|
499
582
|
* 6.53.3: can the hooks on this Mac stop a desk Claude run now? `installed`, or the only
|
|
500
583
|
* difference is a halt-capable earlier script (above). What the cancel route's
|
|
@@ -523,7 +606,9 @@ export function hookHaltReady(status: Pick<HookStatus, 'installed' | 'state' | '
|
|
|
523
606
|
* the wrong run. Claude's own readiness (`hookHaltReady`) is unchanged.
|
|
524
607
|
*/
|
|
525
608
|
export function cursorDeskHaltReady(status: Parameters<typeof hookHaltReady>[0]): boolean {
|
|
526
|
-
|
|
609
|
+
if (!hookHaltReady(status) || typeof status.scriptSha !== 'string') return false
|
|
610
|
+
// X1: the 7.0.31 script reads Cursor's payload exactly as this package's does (they differ only in the wait cap).
|
|
611
|
+
return status.scriptSha === status.packageScriptSha || WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS.includes(status.scriptSha)
|
|
527
612
|
}
|
|
528
613
|
|
|
529
614
|
const INSTALL_COMMAND = 'npx --yes @gotcos/glasses-server@latest --hooks install'
|
|
@@ -646,7 +731,8 @@ export function installClaudeHooks(options: InstallOptions = {}): InstallResult
|
|
|
646
731
|
const packageScriptPath = options.packageScriptPath ?? packagedHookScriptPath()
|
|
647
732
|
const hookPaths = options.hookPaths ?? currentHookPaths()
|
|
648
733
|
const read = readSettings(settingsPath)
|
|
649
|
-
|
|
734
|
+
// X1, 7.0.34: Reinstall hooks is the opt-in, so under the default it writes the long question block.
|
|
735
|
+
const plan = options.plan ?? permissionHookPlan(process.env, { forInstall: true, setting: readQuestionHoldSetting(questionHoldSettingPath(hookPaths.home)) })
|
|
650
736
|
const statusNow = () => hookStatus({ settingsPath, scriptPath, packageScriptPath, hookPaths, plan })
|
|
651
737
|
const refuse = (reason: string, scriptCopied = false): InstallResult => ({ ok: false, reason, changed: false, scriptCopied, backupPath: null, status: statusNow() })
|
|
652
738
|
if (!read.ok) return refuse(read.reason)
|
|
@@ -40,6 +40,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
40
40
|
import { atomicWriteFileSync } from './atomic-fs.js'
|
|
41
41
|
import {
|
|
42
42
|
PERMISSION_HOOK_TIMEOUT_S,
|
|
43
|
+
WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS,
|
|
43
44
|
backupSettings,
|
|
44
45
|
currentHookPaths,
|
|
45
46
|
ensureStableHookScript,
|
|
@@ -264,7 +265,9 @@ export function codexHookStatus(paths: CodexHookPaths = {}): CodexHookStatus {
|
|
|
264
265
|
const hookPaths = paths.hookPaths ?? currentHookPaths()
|
|
265
266
|
const scriptSha = sha256File(scriptPath)
|
|
266
267
|
const packageScriptSha = sha256File(packageScriptPath)
|
|
267
|
-
|
|
268
|
+
// X1: the 7.0.31 script differs from this package's only in the longest question wait it accepts, which Codex never
|
|
269
|
+
// uses (it has no question block), so it stays current for Codex (WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS).
|
|
270
|
+
const scriptOk = scriptSha !== null && (scriptSha === packageScriptSha || WAIT_CAP_ONLY_PRIOR_SCRIPT_SHAS.includes(scriptSha))
|
|
268
271
|
const read = readHooksFile(hooksPath)
|
|
269
272
|
if (!read.ok) {
|
|
270
273
|
return { state: read.reason, installed: false, hooksPath, scriptPath, scriptSha, packageScriptSha, scriptOk, subscribed: [], missing: CODEX_HOOK_SUBSCRIPTIONS.map(s => s.event), drifted: [], duplicated: [] }
|