@gotcos/glasses-server 7.0.31 → 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 +161 -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 +9 -9
- package/server/web-control/app.css +26 -3
- package/server/web-control/app.js +234 -35
- package/server/web-control/control.css +5 -1
- package/server/web-control/index.html +7 -5
- 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,164 @@
|
|
|
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
|
+
|
|
1
162
|
## 7.0.31 - Control Web: a message's text no longer runs under and below Reply on a wide screen
|
|
2
163
|
|
|
3
164
|
2026-11-12. A hotfix to 7.0.30, nothing else in it.
|
|
@@ -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: [] }
|
package/server/lib/full-hold.ts
CHANGED
|
@@ -16,11 +16,33 @@
|
|
|
16
16
|
|
|
17
17
|
import { readSwitch } from './env-switch.js'
|
|
18
18
|
|
|
19
|
-
/** `COS_QUESTION_FULL_HOLD
|
|
19
|
+
/** `COS_QUESTION_FULL_HOLD` set explicitly ON (`1`, `true`, `on`). Unset is the default below, never this. Read per request. */
|
|
20
20
|
export function questionFullHoldEnabled(env: NodeJS.ProcessEnv): boolean {
|
|
21
21
|
return readSwitch(env.COS_QUESTION_FULL_HOLD, false)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* X1, 7.0.34 (Miles 2026-11-13 15:02, "I want the 8 hour question hold ON. please keep it on by default."): the
|
|
26
|
+
* question hold has THREE states, decided in this order:
|
|
27
|
+
* 1. `COS_QUESTION_FULL_HOLD` set to on (`1`, `true`, `on`) or off (`0`, `false`, `off`): that, source `env`.
|
|
28
|
+
* 2. Else the server-side setting Control's Settings row writes (`lib/question-hold-setting.ts`), source `setting`.
|
|
29
|
+
* 3. Else `default`: ON BY DEFAULT, PENDING PER REQUEST. A question is held for the whole wait only when ITS OWN
|
|
30
|
+
* hook stamped a wait longer than DEFAULT_HOLD_MIN_STAMP_S, which only the split question block Reinstall hooks
|
|
31
|
+
* writes ever sends. Until a Mac reinstalls, every request reads exactly as 7.0.33 with the switch off.
|
|
32
|
+
* The env value wins over the setting: it is what the launcher or the person at the terminal set on purpose, and
|
|
33
|
+
* every release since 7.0.16 documents it as the off switch (rollback included). A Settings click must never undo it.
|
|
34
|
+
*/
|
|
35
|
+
export type QuestionHoldMode = 'on' | 'off' | 'default'
|
|
36
|
+
export type QuestionHoldSource = 'env' | 'setting' | 'default'
|
|
37
|
+
export type QuestionHoldSetting = 'on' | 'off'
|
|
38
|
+
|
|
39
|
+
export function questionHoldMode(env: NodeJS.ProcessEnv, setting: QuestionHoldSetting | null = null): { mode: QuestionHoldMode; source: QuestionHoldSource } {
|
|
40
|
+
if (readSwitch(env.COS_QUESTION_FULL_HOLD, false)) return { mode: 'on', source: 'env' }
|
|
41
|
+
if (!readSwitch(env.COS_QUESTION_FULL_HOLD, true)) return { mode: 'off', source: 'env' }
|
|
42
|
+
if (setting === 'on' || setting === 'off') return { mode: setting, source: 'setting' }
|
|
43
|
+
return { mode: 'default', source: 'default' }
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
/** `COS_APPROVAL_FULL_HOLD`: hold a tool approval for the whole hook wait while the desk stays idle. Default OFF. */
|
|
25
47
|
export function approvalFullHoldEnabled(env: NodeJS.ProcessEnv): boolean {
|
|
26
48
|
return readSwitch(env.COS_APPROVAL_FULL_HOLD, false)
|
|
@@ -45,10 +67,16 @@ export function questionRowText(question: unknown): string {
|
|
|
45
67
|
return question.replace(/[\p{Cc}\p{Cf}]+/gu, ' ')
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
|
|
70
|
+
/**
|
|
71
|
+
* `questions`: the question hold is ON outright (env or setting). `questionsByDefault`: it is the 7.0.34 default,
|
|
72
|
+
* which holds a question only when its own hook stamped a long wait (`defaultHoldTakesStamp`). Both false: off.
|
|
73
|
+
* A caller that predates 7.0.34 and passes only `questions` and `approvals` reads as off, exactly as before.
|
|
74
|
+
*/
|
|
75
|
+
export interface FullHoldSwitches { questions: boolean; approvals: boolean; questionsByDefault?: boolean }
|
|
49
76
|
|
|
50
|
-
export function fullHoldSwitches(env: NodeJS.ProcessEnv): FullHoldSwitches {
|
|
51
|
-
|
|
77
|
+
export function fullHoldSwitches(env: NodeJS.ProcessEnv, setting: QuestionHoldSetting | null = null): FullHoldSwitches {
|
|
78
|
+
const q = questionHoldMode(env, setting)
|
|
79
|
+
return { questions: q.mode === 'on', approvals: approvalFullHoldEnabled(env), questionsByDefault: q.mode === 'default' }
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
/**
|
|
@@ -107,9 +135,37 @@ export function engineProvenForFullHold(engine: EngineFacts | null | undefined):
|
|
|
107
135
|
return version !== null && versionAtLeast(version, floor)
|
|
108
136
|
}
|
|
109
137
|
|
|
110
|
-
/** The PermissionRequest wait the installer writes for
|
|
138
|
+
/** The PermissionRequest wait the installer writes for an APPROVAL under the full hold (7.0.16; unchanged). */
|
|
111
139
|
export const FULL_HOLD_HOOK_WAIT_S = 3600
|
|
112
140
|
|
|
141
|
+
/**
|
|
142
|
+
* X1 (Miles 2026-11-12 17:42, "8 hours"): how long a fully held QUESTION stays answerable from the web and the
|
|
143
|
+
* glasses. Everything below is derived from this one number:
|
|
144
|
+
* the question block's `timeout` = FULL_HOLD_QUESTION_WAIT_S = 8 h x 60 min x 60 s = 28,800 s
|
|
145
|
+
* the longest stamp the script takes = 28,800 (5 digits; `bin/hooks/cos-session-hook`, pinned by a test)
|
|
146
|
+
* the longest stamp the broker trusts = HOOK_WAIT_STAMP_MAX_S = 28,800
|
|
147
|
+
* the script's curl gives up at = 28,800 - 5 = 28,795 s
|
|
148
|
+
* the broker's deadline = holdCeilingS = 28,800 - HOOK_REPLY_MARGIN_S (10) = 28,790 s
|
|
149
|
+
* so the deadline stays under the hook's own wait, as every release since 6.52.0 keeps it. Approvals keep
|
|
150
|
+
* FULL_HOLD_HOOK_WAIT_S. The Mac's own dialog has no limit past this: only the remote answer ends.
|
|
151
|
+
*/
|
|
152
|
+
export const FULL_HOLD_QUESTION_WAIT_HOURS = 8
|
|
153
|
+
export const FULL_HOLD_QUESTION_WAIT_S = FULL_HOLD_QUESTION_WAIT_HOURS * 60 * 60
|
|
154
|
+
/** The question block 7.0.16 to 7.0.31 wrote: an install from then reads as an earlier wait (`priorWaitOnly`), still halt-ready. */
|
|
155
|
+
export const FULL_HOLD_QUESTION_WAIT_PRIOR_S = 3600
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* X1, 7.0.34: under the default, a question is held for the whole wait only when its hook stamped MORE than this.
|
|
159
|
+
* The one block 6.60.0 to 7.0.33 writes stamps 630 and the 7.0.16 to 7.0.31 question block 3600, so neither does;
|
|
160
|
+
* only the split question block Reinstall hooks writes under 7.0.32 or later (28,800) does. The gate is the
|
|
161
|
+
* invocation's own stamp, so a session still running on hooks it loaded before Reinstall keeps 7.0.33's behaviour.
|
|
162
|
+
*/
|
|
163
|
+
export const DEFAULT_HOLD_MIN_STAMP_S = FULL_HOLD_QUESTION_WAIT_PRIOR_S
|
|
164
|
+
|
|
165
|
+
export function defaultHoldTakesStamp(hookWaitS: number | null | undefined): boolean {
|
|
166
|
+
return typeof hookWaitS === 'number' && Number.isFinite(hookWaitS) && hookWaitS > DEFAULT_HOLD_MIN_STAMP_S
|
|
167
|
+
}
|
|
168
|
+
|
|
113
169
|
/**
|
|
114
170
|
* The matcher of the non-question PermissionRequest block once the hook is split (canary C9:
|
|
115
171
|
* honoured on both engines; a question runs only the `AskUserQuestion` block, everything else
|