@gotcos/glasses-server 6.53.3 → 6.54.0
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 +15 -0
- package/CHANGELOG.md +30 -0
- package/README.md +91 -1
- package/bin/hooks/cos-session-hook +18 -9
- package/managed-runtime-contract.json +4 -1
- package/package.json +1 -1
- package/server/index.ts +7 -0
- package/server/lib/attached-provider-adapter.ts +8 -0
- package/server/lib/claude-hooks-installer.ts +12 -6
- package/server/lib/fence-liveness.ts +76 -2
- package/server/lib/lens-gist-engines.ts +416 -0
- package/server/lib/lens-gist.ts +893 -0
- package/server/lib/provider-proof.ts +4 -2
- package/server/lib/session-halt.ts +59 -12
- package/server/lib/session-hooks-runtime.ts +10 -3
- package/server/routes/agent-session-bindings.ts +8 -16
- package/server/routes/health.ts +3 -0
- package/server/routes/lens-gist.ts +58 -0
package/.env.example
CHANGED
|
@@ -252,6 +252,21 @@ BIND_HOST=0.0.0.0
|
|
|
252
252
|
# tier for the rest of the day. Default 40.
|
|
253
253
|
# COS_MEETING_SUMMARY_DAILY_CAP=40
|
|
254
254
|
|
|
255
|
+
# ── LENS GIST: the summary behind the G2 prompt box (6.54.0) ────────────
|
|
256
|
+
# Outcome / So what / You asked for a finished session or message, written by the
|
|
257
|
+
# engine you pick: claude (default, when the Claude CLI is installed), codex, cursor,
|
|
258
|
+
# ollama, or off. A mistyped engine is off. PUT /api/lens-gist/config saves the same
|
|
259
|
+
# choice in the data dir; this setting wins over it.
|
|
260
|
+
# COS_LENS_GIST_ENGINE=claude
|
|
261
|
+
# COS_LENS_GIST_MODEL=sonnet
|
|
262
|
+
#
|
|
263
|
+
# Or a chain, asked in order when one fails, cools down or reports a usage limit.
|
|
264
|
+
# Each engine in it sees the reply. COS_LENS_GIST_MODEL does not apply to a chain.
|
|
265
|
+
# COS_LENS_GIST_ENGINE=claude:sonnet,codex:gpt-5.6-terra
|
|
266
|
+
#
|
|
267
|
+
# Answers per local day (attempts stop at twice this). Default 150.
|
|
268
|
+
# COS_LENS_GIST_DAILY_CAP=150
|
|
269
|
+
|
|
255
270
|
|
|
256
271
|
# ── RETAINED MEETING-AUDIO PLAYBACK CLEANUP (6.21.32 canary) ────────────
|
|
257
272
|
# Default OFF. When enabled, the first authenticated Play of a retained raw
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 6.54.0
|
|
2
|
+
|
|
3
|
+
### The summary behind the G2 prompt box, from the engine you choose
|
|
4
|
+
|
|
5
|
+
COS Glasses 6.9.545 floats your words over a dimmed card of the session or message you are replying to. The server now writes that card: `Outcome` (`Answer` for a message), `So what` and `You asked`, from a reply the phone already holds. The engine is swappable the way the COS indexer's is: Claude, Codex, Cursor or a local Ollama model.
|
|
6
|
+
|
|
7
|
+
- **`POST /api/lens-gist`** `{kind, ask, reply}` answers `ready` with the three lines, or `off`, `unavailable` (`cap`, `breaker`, `busy`, `retry-later`, `maintenance`) or `failed`, always as a 200 with a status; only a malformed request is a 400. **`GET /api/lens-gist/config`** lists the engines, whether each is installed, and the models it can run; **`PUT`** saves `{engine, model, dailyCap}` to `<data>/lens-gist.json`, which survives Update Server.
|
|
8
|
+
- **The engine:** `COS_LENS_GIST_ENGINE` and `COS_LENS_GIST_MODEL`, then the saved choice, then Claude Sonnet when the Claude CLI is installed (off otherwise). One engine never falls back to another; a mistyped one, or a saved file that cannot be read, is off.
|
|
9
|
+
- **A chain, for resilience:** `claude:sonnet,codex:gpt-5.6-terra` (env, or `PUT {"chain": ...}`) asks the next engine when one fails, is cooling down, or reports a spent session or usage limit, which opens that engine's breaker at once. A limit is read only from the provider's own error (the same pattern as the provider proof, including Claude Code's "You've hit your session limit", which it prints on stdout), never from the words of an answer. The chain shares one 70 s budget, and no engine starts on its last 3 s. A refusal from every engine reads as `breaker` only when every engine's breaker is open, so one engine's hiccup never pauses the phone. Opt-in only: a chain sends the reply to every provider in it; `PUT` takes either `engine` or `chain`, and a saved `off` wins. An answer any engine wrote is reused for the whole chain. Tested live: with Claude failing fast (a model it does not have), Codex Terra answered in 4.9 s end to end.
|
|
10
|
+
- **Measured with the shipped prompt** on real replies: Claude Sonnet 2 to 5 s on about 1.2k tokens; Codex `gpt-5.6-terra` (the Codex default, the peer of Sonnet) 3.2 to 3.7 s on about 11.4k, `gpt-6-luna` and `gpt-6-sol` about 3 s on about 12k (Codex's own harness); Cursor `grok-4.7-low-fast` and `composer-2.5-fast` 11 to 13 s on about 11k to 17k; Ollama `qwen3.8:27b` 14 s cold. Claude Haiku timed out at 30 s twice.
|
|
11
|
+
- **The reply is untrusted text.** Claude runs with no tools and every hook disabled; Codex with its shell, exec, image, agent, browser, computer, app and plugin tools disabled, web search off, no user config and no rules files; Cursor in ask mode (read-only tools) in an empty workspace; Ollama as a bare completion. Each call gets a private empty workspace, removed after it, and no server token in its environment. Text inside the exchange cannot close its own tag. Keys and tokens (OpenAI, GitHub, Slack, AWS, HubSpot, JWT, private keys and the server's own token) are redacted from cards, logs and the ledger.
|
|
12
|
+
- **How it stops:** answers cached on disk per reply, engine and model; a daily cap of 150 answers (`COS_LENS_GIST_DAILY_CAP` or the saved `dailyCap`, kept when a save does not mention it) and twice that in attempts; a failed reply refused for 10 minutes, then 6 hours (a spent provider limit does not count against it); a breaker per engine (3 failures in a row or one limit answer, 30 minutes, then one trial call); at most 2 running and 4 waiting, with the cap, breaker and drain checked again before each engine. The route sits outside the request lease, so a slow model never holds up Update Server, and it answers `maintenance` during a drain.
|
|
13
|
+
- **Visible:** a `g2-lens-gist` row in the token audit for every call, `<data>/lens-gist-runs.jsonl` (with `fallbackFrom` and `limit` when they apply), and `lens_gist` on `/api/health` (engine, model, calls today, cap, open breakers; no error text and no chain on the unauthenticated slice). Error text is redacted before it is cut, so a cut cannot split a token past the redaction.
|
|
14
|
+
- Stated limits: a session reply reaches the phone cut to its first 4,000 characters, so a long reply's ending is not summarized. Cursor's ask mode keeps read-only tools; the CLI has no switch to remove them. COS Control 0.5.239 drops a `COS_LENS_GIST_*` value set only in the plist; use `~/.cos-glasses/.env` or `PUT /api/lens-gist/config`.
|
|
15
|
+
- Checks: three /qa rounds (four reviewers; one per repo on the fixes; two on the chain before release). The limit reading is tested against real processes printing Claude Code's own failure shapes. 50 mutation cases, all killed from a green baseline. Full suite 5,214 passed, 2 skipped (343 files); typecheck clean; `npm audit --omit=dev` 0 vulnerabilities.
|
|
16
|
+
|
|
17
|
+
## 6.53.4
|
|
18
|
+
|
|
19
|
+
### The fixes from 6.53.3's review
|
|
20
|
+
|
|
21
|
+
6.53.3 was published before its four-reviewer /qa round finished (the round was cut short by a usage limit and rerun the next morning). This release carries that round's server fixes, and corrects three claims 6.53.3 made. Install hooks once after updating.
|
|
22
|
+
|
|
23
|
+
- **A `ps` that fails never releases a fence (blocker in 6.53.3).** 6.53.3's fence auto-release read every `execFileSync` error as "not running", so a `ps` killed at its 2 s timeout, an `EAGAIN` fork failure or a missing binary could release a cancel fence whose child might still be writing. Only the measured "no such process" answer (exit 1, no signal, not killed, no error code, nothing on stdout or stderr) counts as gone now; everything else, and a clean exit that printed nothing, is unverifiable and keeps the fence. `ps` is `/bin/ps` when present. A fence that reads unknown is re-checked on every fence check and can still be released by hand in COS Control.
|
|
24
|
+
- **The halt re-arm matches the delivered prompt exactly.** 6.53.3 matched the first 120 characters with `includes`, so a short Continue ("yes") matched the person's own next desk prompt: their run was stopped "Cancelled from COS" and the cancelled COS turn then ran. Now the words must be equal (whitespace collapsed, Claude Code's peer-message wrapper taken off), and the first prompt after the hand-off decides: a different prompt spends the re-arm and is logged. Stated limit: if the person's own prompt reaches the session first, the queued COS turn is not stopped; an identical desk prompt typed after the send and taken first still re-arms. An unverified hand-off holds its re-arm 2 minutes, not an hour.
|
|
25
|
+
- **The hook drains stdin on every event.** PermissionRequest, Stop, PostToolUse, SessionStart, SessionEnd and the other non-front events read only 1 MiB and left the rest unread, and the spool guard exited before reading; a larger payload could EPIPE the writer. The header comment no longer says the PreToolUse branch writes no spool. Script sha256 `df54677f...`; the 6.53.0 script (`1158bb06...`) and 6.53.3's (`c0b41bf8...`) still stop desk runs until Install hooks is run. The PreToolUse common path is unchanged (40 runs each, interleaved: 11.2 and 10.9 ms before, 11.4 and 11.0 ms after).
|
|
26
|
+
- **The mutation gate needs a failing test.** A kill was any non-zero vitest exit; now a mutant is killed only when a named test fails, and a run that errors without one fails the gate. Every 6.53.x case was re-run under that rule on a green baseline: 137 of 137 killed by a failing test (16 added in this round), none errored.
|
|
27
|
+
- **Every server suite removes its temp folders.** 39 test files made `mkdtemp` folders and never removed them (one run left 581 entries in `$TMPDIR`; now none made by a test). 6.53.3's "hook and installer suites clean up their temp roots" was true only for three suites.
|
|
28
|
+
- **Corrections to 6.53.3's notes.** "`reaped: true` is never reported while an owned process survives" holds for cancel and timeout only: a natural exit settles as soon as the CLI is reaped, with no tree probe. The graceful end-to-end bound is now derived from the fixture's stop delay and `TREE_POLL_MS` (2.4 s, was 4 s). The closed-session test imports its retention constant. README gains a "Cancel a run from the glasses (6.53.x)" section.
|
|
29
|
+
- Tests: full suite 5,128 passed, 2 skipped; typecheck clean; `npm audit --omit=dev` 0 vulnerabilities.
|
|
30
|
+
|
|
1
31
|
## 6.53.3
|
|
2
32
|
|
|
3
33
|
### Cancel owns the whole process tree, desk cancels hold, and the hook is faster
|
package/README.md
CHANGED
|
@@ -265,6 +265,95 @@ Allow once or deny only: no permission rule is ever written. Rows carry
|
|
|
265
265
|
row reads `waiting_kind: question` whatever the switch says; `/api/health` reports
|
|
266
266
|
`permissionBroker` (counters and timestamps, no ids or text).
|
|
267
267
|
|
|
268
|
+
## Cancel a run from the glasses (6.53.x)
|
|
269
|
+
|
|
270
|
+
The glasses' Cancel run row stops what a session is doing, the way the desk's own stop
|
|
271
|
+
button does. It needs **server 6.53.0 or later and COS Glasses 6.9.529 or later**;
|
|
272
|
+
**6.53.4 is recommended** (it owns the whole process tree, holds a desk cancel, fixes
|
|
273
|
+
the false fences 6.53.1 left, and never releases a fence when `ps` cannot answer).
|
|
274
|
+
|
|
275
|
+
- **A turn COS started** (`cos_turn`) is stopped by the server: its whole process tree is
|
|
276
|
+
signalled and the turn settles `turn_cancelled`.
|
|
277
|
+
- **A run at the desk** (`desk_run`: a Desktop tab or a terminal `claude`) is stopped by the
|
|
278
|
+
session hook at its next tool call, so a reply that is pure text finishes first. This
|
|
279
|
+
needs the hooks installed, and after every server update that changes the hook script
|
|
280
|
+
(6.53.0, 6.53.3, 6.53.4) you must **run Install hooks once** (COS Control, or
|
|
281
|
+
`npx --yes @gotcos/glasses-server@latest --hooks install`). Until then
|
|
282
|
+
`--hooks status` reads `script_outdated` and its `advice` line says so; a script from
|
|
283
|
+
6.53.0 to 6.53.3 can still stop desk runs meanwhile, anything older answers
|
|
284
|
+
`hooks_outdated`. The PreToolUse hook runs before every tool call on the Mac and never
|
|
285
|
+
contacts the server; it writes a spool file only for AskUserQuestion, ExitPlanMode and
|
|
286
|
+
prompts, and always drains its input.
|
|
287
|
+
- **Codex and Cursor** runs at the desk answer `cancel_unsupported` ("Stop it there").
|
|
288
|
+
- **Fences a cancel left release themselves** once every process the cancelled turn
|
|
289
|
+
started is confirmed gone: only `ps` answering "no such process" counts, and a `ps` that
|
|
290
|
+
times out, cannot start or is missing keeps the fence. Any other fence is still released
|
|
291
|
+
by hand (`GET /api/agent-sessions/fences`, then `POST .../fences/release` with
|
|
292
|
+
`confirm: true`).
|
|
293
|
+
- **A cancel that lands while COS is handing a turn to the open session** writes the desk
|
|
294
|
+
marker, and re-arms it once if the next prompt that session starts is exactly the
|
|
295
|
+
delivered one. If your own prompt reaches the session first, the re-arm is dropped (and
|
|
296
|
+
logged) rather than stop your run.
|
|
297
|
+
|
|
298
|
+
Meetings: a closed meeting's record ages out after **4 hours**, after which its status
|
|
299
|
+
reads `missing` and the phone stops asking to save it. Saving a known meeting that has no
|
|
300
|
+
transcript answers **404 `nothing_to_save`** (with its state); only an unknown id answers
|
|
301
|
+
`session_not_found`.
|
|
302
|
+
|
|
303
|
+
## The summary behind the prompt box (6.54.0)
|
|
304
|
+
|
|
305
|
+
When you hold the ring on a finished session or message, COS Glasses 6.9.545 floats your
|
|
306
|
+
live words over a dimmed card of what you are replying to. From 6.54.0 the server writes
|
|
307
|
+
that card: `Outcome` (`Answer` for a message), `So what` and `You asked`, one lens row
|
|
308
|
+
each, instead of the reply's first lines. Older phones and older servers keep the
|
|
309
|
+
6.9.544 card.
|
|
310
|
+
|
|
311
|
+
**Pick the engine** the way the COS indexer does: `COS_LENS_GIST_ENGINE` wins, then the
|
|
312
|
+
choice saved with `PUT /api/lens-gist/config`, then the default. One engine never falls back
|
|
313
|
+
to another; `off` turns the summary off.
|
|
314
|
+
|
|
315
|
+
**Or chain them, for resilience.** `claude:sonnet,codex:gpt-5.6-terra` asks Claude first and
|
|
316
|
+
Codex when Claude fails, is cooling down, or says its session or usage limit is spent (a
|
|
317
|
+
limit, read from the provider's own error and never from an answer, skips that engine for 30
|
|
318
|
+
minutes at once). Running low on one plan no longer turns the summary off. A chain gets 70
|
|
319
|
+
seconds in all, shared in order. It is opted into, never the default: it sends a session's
|
|
320
|
+
reply to every provider in it, and an installed CLI is not consent (ChatGPT.app ships
|
|
321
|
+
`codex`). `PUT` takes either `engine` or `chain`, never both, and a saved `off` wins.
|
|
322
|
+
|
|
323
|
+
| Engine | Default model | Measured (2026-09-25, one call) | Tools it keeps |
|
|
324
|
+
|---|---|---|---|
|
|
325
|
+
| `claude` (default) | `sonnet` | 2 to 5 s, about 1.2k tokens | none (`--tools ""`, hooks off) |
|
|
326
|
+
| `codex` | `gpt-5.6-terra` (`gpt-6-luna`, `gpt-6-sol` work as well) | 3.2 to 3.7 s, about 11.4k tokens | shell, browser, image, agent, app and plugin tools off; web search off |
|
|
327
|
+
| `cursor` | `grok-4.7-low-fast` | 11 to 13 s, about 11k to 17k tokens | ask mode: read-only tools in an empty workspace |
|
|
328
|
+
| `ollama` | the local model lens queries use | 2.5 s warm, 14 s cold | none |
|
|
329
|
+
|
|
330
|
+
With nothing chosen and no Claude CLI installed, the summary is off rather than sent to a
|
|
331
|
+
provider nobody picked. Claude `haiku` timed out at 30 s in testing and is not a default.
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
curl -s -X PUT -H "X-COS-Token: $COS_API_TOKEN" -H 'content-type: application/json' \
|
|
335
|
+
-d '{"chain":"claude:sonnet,codex:gpt-5.6-terra","dailyCap":150}' http://127.0.0.1:3141/api/lens-gist/config
|
|
336
|
+
curl -s -X PUT -H "X-COS-Token: $COS_API_TOKEN" -H 'content-type: application/json' \
|
|
337
|
+
-d '{"engine":"codex","model":"gpt-6-sol"}' http://127.0.0.1:3141/api/lens-gist/config
|
|
338
|
+
curl -s -H "X-COS-Token: $COS_API_TOKEN" http://127.0.0.1:3141/api/lens-gist/config
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`GET /api/lens-gist/config` lists every engine, whether it is installed, and the models it
|
|
342
|
+
can run here. The saved choice lives in `<data>/lens-gist.json` and survives Update
|
|
343
|
+
Server; a file that cannot be read turns the summary off until it is saved again.
|
|
344
|
+
|
|
345
|
+
**How it stops.** The phone asks once per finished reply it has open (a session that is
|
|
346
|
+
still running or waiting on an approval asks for nothing). Answers are cached on disk per
|
|
347
|
+
engine and model. A daily cap (`COS_LENS_GIST_DAILY_CAP` or the saved `dailyCap`, default
|
|
348
|
+
150) counts answers, and attempts stop at twice that. A reply that failed is refused for 10
|
|
349
|
+
minutes, then 6 hours (a spent provider limit does not count against the reply). Three
|
|
350
|
+
failures in a row, or one limit answer, pause that engine for 30 minutes, then one trial call
|
|
351
|
+
runs. At most 2 calls run at once with 4 waiting. A drain for Update Server stops queued
|
|
352
|
+
calls before each engine. Every call writes a `g2-lens-gist` row to the token audit and a line
|
|
353
|
+
to `<data>/lens-gist-runs.jsonl` (with `fallbackFrom` when a later engine answered).
|
|
354
|
+
`/api/health` shows `lens_gist` (engine, model, calls today, cap, open breakers; no error text
|
|
355
|
+
and no chain); `GET /api/lens-gist/config` shows the chain.
|
|
356
|
+
|
|
268
357
|
## Configuration
|
|
269
358
|
|
|
270
359
|
Config lives at `~/.cos-glasses/.env` (created on first run). Every key is
|
|
@@ -626,7 +715,8 @@ BIND_HOST=0.0.0.0 npm run start:server
|
|
|
626
715
|
timers are frozen), `desk_active` (the Mac saw input), `approvals_off`
|
|
627
716
|
(`COS_PERMISSION_BROKER=questions`), `broker_off`, `cursor`, `unsupported_tool`
|
|
628
717
|
(ExitPlanMode is never held). `lastQuestionsPollAt` says when a client last counted.
|
|
629
|
-
`--hooks status` must read `installed
|
|
718
|
+
`--hooks status` must read `installed` (right after updating to 6.53.4 it reads
|
|
719
|
+
`script_outdated` until Install hooks is run once; its `advice` line says so). To turn it off with no reinstall, set
|
|
630
720
|
`COS_PERMISSION_BROKER=0` in `~/.cos-glasses/.env` and restart. For the Messages
|
|
631
721
|
trail, `/api/health` `messages_trail.readerErrors` counts lines a reader could not map.
|
|
632
722
|
- *Offline meeting recovery unavailable?* — build 209+ requires server 6.11.0+.
|
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
# restarting never delays a Claude session.
|
|
8
8
|
#
|
|
9
9
|
# 6.53.0: PreToolUse is SYNCHRONOUS with no matcher, so the first branch below runs before
|
|
10
|
-
# EVERY tool call in every Claude session on this Mac. It
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
10
|
+
# EVERY tool call in every Claude session on this Mac. It never contacts the server. For an
|
|
11
|
+
# ordinary tool it writes nothing at all; it spools only AskUserQuestion and ExitPlanMode
|
|
12
|
+
# (the two tools the reducer reads, the 6.48 matcher's set), and UserPromptSubmit, which
|
|
13
|
+
# shares the branch, is always spooled. It prints nothing unless the server left a halt
|
|
14
|
+
# marker for this session, when it prints the one reply that stops the run (canary C1b,
|
|
15
|
+
# 2026-09-21: `continue:false` alone still ran the tool; with the deny it did not). It never
|
|
16
|
+
# exits 2: exit 2 blocks the tool, while a timeout or any other code fails open (C6, C7).
|
|
17
|
+
# Every path, every event, drains stdin before it exits, so Claude Code's writer never
|
|
18
|
+
# EPIPEs whatever the payload's size (6.53.3; every other event since its /qa round).
|
|
15
19
|
#
|
|
16
20
|
# The installer bakes COS_GLASSES_HOME and COS_HOOK_SPOOL into the command, so this
|
|
17
21
|
# script and the server can never disagree about where the token or the spool is.
|
|
@@ -89,6 +93,9 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
|
|
|
89
93
|
trap 'cat >/dev/null 2>&1' EXIT
|
|
90
94
|
;;
|
|
91
95
|
esac
|
|
96
|
+
# Every other event: until the spool read below has taken stdin, any exit (a failed mkdir,
|
|
97
|
+
# the B8 guard) drains it, as the front does.
|
|
98
|
+
[ -n "$FRONT" ] || trap 'cat >/dev/null 2>&1' EXIT
|
|
92
99
|
mkdir -p "$SPOOL/rejected" 2>/dev/null || exit 0
|
|
93
100
|
TS=$(perl -MTime::HiRes -e 'printf "%.0f", Time::HiRes::time()*1000' 2>/dev/null || date +%s000)
|
|
94
101
|
# B8 guard: a full spool or a stale drain stamp means nobody is reading; keep only permissions.
|
|
@@ -97,13 +104,15 @@ if [ "$EVENT" != PermissionRequest ]; then
|
|
|
97
104
|
STAMP="$SPOOL/.last-drain"; [ ! -f "$STAMP" ] || [ -z "$(find "$STAMP" -mmin +1440 2>/dev/null)" ] || exit 0 # -mtime +1 would mean two days
|
|
98
105
|
fi
|
|
99
106
|
T="$SPOOL/.tmp.$$.$TS"
|
|
107
|
+
# Stdin to disk with a 1 MiB cap; whatever lies past the cap is drained, never left for the
|
|
108
|
+
# writer to EPIPE on.
|
|
109
|
+
trap - EXIT
|
|
100
110
|
if [ -n "$FRONT" ]; then
|
|
101
|
-
# 6.53.3: the 4 KiB the halt check read, then the rest of stdin
|
|
102
|
-
# 1 MiB cap; whatever lies past the cap is drained, never left for the writer to EPIPE on.
|
|
103
|
-
trap - EXIT
|
|
111
|
+
# 6.53.3: the 4 KiB the halt check read, then the rest of stdin.
|
|
104
112
|
{ printf '%s' "$HEAD"; exec cat; } | { head -c 1048576 > "$T.in"; exec cat >/dev/null; }
|
|
105
113
|
else
|
|
106
|
-
|
|
114
|
+
# No `exec` here: this group runs in the script's own shell.
|
|
115
|
+
{ head -c 1048576 > "$T.in"; cat >/dev/null; }
|
|
107
116
|
fi
|
|
108
117
|
case "$(head -c 1 "$T.in")" in '{') ;; *) EVENT=rejected ;; esac
|
|
109
118
|
{ printf '{"ts":%s,"ppid":%s,"event":"%s","payload":' "$TS" "$PPID" "$EVENT"; cat "$T.in"; printf '}\n'; } > "$T"
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
"COS_SESSION_HOOK_SSE",
|
|
35
35
|
"COS_PERMISSION_BROKER",
|
|
36
36
|
"COS_PERMISSION_BROKER_DESK_IDLE_S",
|
|
37
|
-
"COS_PERMISSION_BROKER_TIMEOUT_S"
|
|
37
|
+
"COS_PERMISSION_BROKER_TIMEOUT_S",
|
|
38
|
+
"COS_LENS_GIST_ENGINE",
|
|
39
|
+
"COS_LENS_GIST_MODEL",
|
|
40
|
+
"COS_LENS_GIST_DAILY_CAP"
|
|
38
41
|
],
|
|
39
42
|
"maintenance": {
|
|
40
43
|
"scope": "cross_boot",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.54.0",
|
|
4
4
|
"description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/index.ts
CHANGED
|
@@ -92,6 +92,7 @@ import { glossaryRouter } from './routes/glossary.js'
|
|
|
92
92
|
import { handoffsRouter } from './routes/handoffs.js'
|
|
93
93
|
import { recoveryRouter } from './routes/recovery.js'
|
|
94
94
|
import { promptEditRouter } from './routes/prompt-edit.js'
|
|
95
|
+
import { lensGistRouter } from './routes/lens-gist.js'
|
|
95
96
|
import { bookmarksRouter } from './routes/bookmarks.js'
|
|
96
97
|
import { welcomeContextRouter } from './routes/welcome-context.js'
|
|
97
98
|
import { liveCuesRouter } from './routes/live-cues.js'
|
|
@@ -311,6 +312,11 @@ app.use('/api', (req, res, next) => {
|
|
|
311
312
|
|| req.path.startsWith('/meeting-actions/')
|
|
312
313
|
|| req.path === '/meeting-engine/mode'
|
|
313
314
|
|| req.path.startsWith('/prompt-drafts')
|
|
315
|
+
// 6.54.0: a gist can wait behind two others and then run a chain for up to 70 s
|
|
316
|
+
// (LENS_GIST_CHAIN_BUDGET_MS), which could keep Control's 90 s drain waiting if it held
|
|
317
|
+
// the request lease. It refuses on its own while admissions are closed, and a queued
|
|
318
|
+
// chain checks again before each engine (routes/lens-gist.ts, lib/lens-gist.ts).
|
|
319
|
+
|| req.path === '/lens-gist'
|
|
314
320
|
|| req.path.startsWith('/maintenance/drain')
|
|
315
321
|
if (lifecycleOwned) return next()
|
|
316
322
|
|
|
@@ -889,6 +895,7 @@ app.use('/api', glossaryRouter)
|
|
|
889
895
|
app.use('/api', handoffsRouter)
|
|
890
896
|
app.use('/api', recoveryRouter)
|
|
891
897
|
app.use('/api', promptEditRouter)
|
|
898
|
+
app.use('/api', lensGistRouter)
|
|
892
899
|
app.use('/api', bookmarksRouter)
|
|
893
900
|
app.use('/api', welcomeContextRouter)
|
|
894
901
|
app.use('/api', liveCuesRouter)
|
|
@@ -186,6 +186,14 @@ export interface AttachedTurnFailureResult {
|
|
|
186
186
|
/**
|
|
187
187
|
* Did `close`/`exit` actually fire, i.e. did the kernel reap the child?
|
|
188
188
|
*
|
|
189
|
+
* WHAT IT DOES AND DOES NOT PROMISE (6.53.3, scoped by its /qa round). On a CANCEL or a
|
|
190
|
+
* TIMEOUT, `reaped: true` also means the whole owned process tree was read gone after
|
|
191
|
+
* `close` (the tree poll); a tree that outlives SIGKILL settles `reaped: false`. On a
|
|
192
|
+
* NATURAL exit (the provider closed without a cancel or timeout) it means only that the
|
|
193
|
+
* CLI itself was reaped: that path settles at once, with no tree probe, so a detached
|
|
194
|
+
* background job the tool left running is not looked for. A pre-spawn cancel reports
|
|
195
|
+
* `reaped: true` because no process ever existed.
|
|
196
|
+
*
|
|
189
197
|
* NOT DERIVABLE FROM `exitCode`. A child killed by a signal reports
|
|
190
198
|
* `code === null`, and the handlers below only assign `exitCode` for a numeric
|
|
191
199
|
* code — so the dominant timeout shape (SIGTERM, then SIGKILL) is reaped while
|
|
@@ -285,15 +285,21 @@ export function hookStatus(paths: { settingsPath?: string; scriptPath?: string;
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
|
-
* 6.53.3: sha256 of every EARLIER script that already carries the halt check
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
288
|
+
* 6.53.3: sha256 of every EARLIER script that already carries the halt check. 6.53.3
|
|
289
|
+
* rewrote the script for speed (review A, option a), so an install that has not run Install
|
|
290
|
+
* hooks since reads `script_outdated` and Control shows its banner. Those scripts still stop
|
|
291
|
+
* a desk run exactly as before, though, so a desk cancel is not refused `hooks_outdated` in
|
|
292
|
+
* the meantime (`hookHaltReady`). A script older than 6.53.0 has no halt check and never
|
|
293
|
+
* qualifies.
|
|
294
|
+
* - 1158bb06...: the one script 6.53.0, 6.53.1 and 6.53.2 all shipped.
|
|
295
|
+
* - c0b41bf8...: the 6.53.3 script as first built, before its /qa round made every event
|
|
296
|
+
* drain stdin. Never published, but installable from a local build of that commit
|
|
297
|
+
* (7433b7f), and it carries the same halt check.
|
|
298
|
+
* Each is pinned against its bytes in `server/lib/__fixtures__/`.
|
|
294
299
|
*/
|
|
295
300
|
export const HALT_CAPABLE_PRIOR_SCRIPT_SHAS: readonly string[] = [
|
|
296
301
|
'1158bb06297550128f01fc47caa68806a5287d6da2d05b0d1c812e8ae20764e7',
|
|
302
|
+
'c0b41bf8581cfea498e1e1ac5220fe4e148bd97448d13b60a88879ee5992cc51',
|
|
297
303
|
]
|
|
298
304
|
|
|
299
305
|
/**
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
// human deciding whether to release; an over-confident `none_running` is worse than
|
|
28
28
|
// admitting the probe could not see.
|
|
29
29
|
|
|
30
|
+
import { execFileSync } from 'node:child_process'
|
|
31
|
+
import { existsSync } from 'node:fs'
|
|
32
|
+
|
|
30
33
|
/** Recorded spawn: a pid and the start COS measured when it created the child. */
|
|
31
34
|
export interface RecordedSpawn {
|
|
32
35
|
pid: number
|
|
@@ -55,7 +58,7 @@ export interface FenceLiveness {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface FenceLivenessDeps {
|
|
58
|
-
/** Epoch ms at which `pid` started, or null when it is not running.
|
|
61
|
+
/** Epoch ms at which `pid` started, or null ONLY when it is provably not running.
|
|
59
62
|
* MAY THROW; a throwing probe is not an answer and resolves to `unknown`. */
|
|
60
63
|
pidStartMs: (pid: number) => number | null
|
|
61
64
|
}
|
|
@@ -116,7 +119,9 @@ export function makePidStartProbe(
|
|
|
116
119
|
const raw = run(pid)
|
|
117
120
|
if (raw === null) return null
|
|
118
121
|
const text = raw.trim()
|
|
119
|
-
|
|
122
|
+
// A clean exit that printed nothing is not "not running" (only the runner's null is):
|
|
123
|
+
// throw, so it is counted unverifiable.
|
|
124
|
+
if (!text) throw new Error('empty process start from ps')
|
|
120
125
|
const parsed = Date.parse(text)
|
|
121
126
|
// Unparseable is NOT "not running" -- throw so the caller counts it
|
|
122
127
|
// unverifiable rather than silently reading a live child as gone.
|
|
@@ -124,3 +129,72 @@ export function makePidStartProbe(
|
|
|
124
129
|
return parsed
|
|
125
130
|
}
|
|
126
131
|
}
|
|
132
|
+
|
|
133
|
+
/** What this module reads off the error `execFileSync` throws. */
|
|
134
|
+
export interface ExecFailureLike {
|
|
135
|
+
status?: unknown
|
|
136
|
+
signal?: unknown
|
|
137
|
+
killed?: unknown
|
|
138
|
+
code?: unknown
|
|
139
|
+
error?: unknown
|
|
140
|
+
stdout?: unknown
|
|
141
|
+
stderr?: unknown
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The slice of `execFileSync` the probe calls. Injectable so a test can throw every error
|
|
145
|
+
* shape Node produces without spawning anything. */
|
|
146
|
+
export type PsExec = (file: string, args: readonly string[], options: { encoding: 'utf8'; timeout: number }) => string
|
|
147
|
+
|
|
148
|
+
/** How long one `ps` may take before Node kills it (the kill is NOT an answer). */
|
|
149
|
+
export const PS_PROBE_TIMEOUT_MS = 2_000
|
|
150
|
+
|
|
151
|
+
const PS_BIN = existsSync('/bin/ps') ? '/bin/ps' : 'ps'
|
|
152
|
+
|
|
153
|
+
const emptyOutput = (value: unknown): boolean =>
|
|
154
|
+
(typeof value === 'string' && value.trim() === '') || (Buffer.isBuffer(value) && value.length === 0)
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 6.53.3 /qa BLOCKER: is this `execFileSync` failure `ps -p` saying "no such process"?
|
|
158
|
+
*
|
|
159
|
+
* THE ONE ANSWER THAT MEANS GONE. `ps -p <pid>` for a pid that is not running exits 1 on its
|
|
160
|
+
* own and prints nothing on either stream (measured, macOS 26 and node 24: `{status: 1,
|
|
161
|
+
* signal: null, stdout: '', stderr: ''}` with no `code` and no `error`). Everything else is
|
|
162
|
+
* ps failing to answer, and says nothing about the child: a kill at the timeout (`ETIMEDOUT`,
|
|
163
|
+
* SIGTERM), a fork that failed (`EAGAIN`), no binary (`ENOENT`), a signal, output on stdout or
|
|
164
|
+
* a complaint on stderr, any other status. Until 6.53.3's QA every one of those read as gone,
|
|
165
|
+
* and gone is `none_running`, which releases a cancel fence (`releaseSettledCancelFences`) on a
|
|
166
|
+
* child that may still be writing.
|
|
167
|
+
*/
|
|
168
|
+
export function psSaysNoSuchProcess(error: unknown): boolean {
|
|
169
|
+
if (error === null || typeof error !== 'object') return false
|
|
170
|
+
const e = error as ExecFailureLike
|
|
171
|
+
return e.status === 1
|
|
172
|
+
&& (e.signal === null || e.signal === undefined)
|
|
173
|
+
&& e.killed !== true
|
|
174
|
+
&& (e.code === null || e.code === undefined)
|
|
175
|
+
&& (e.error === null || e.error === undefined)
|
|
176
|
+
&& emptyOutput(e.stdout)
|
|
177
|
+
&& emptyOutput(e.stderr)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The production runner: `ps -p <pid> -o lstart=`'s stdout, or null ONLY when ps said the
|
|
182
|
+
* process does not exist (`psSaysNoSuchProcess`). Any other failure THROWS, which
|
|
183
|
+
* `fenceLiveness` counts unverifiable, so the fence stays.
|
|
184
|
+
*/
|
|
185
|
+
export function psLstartRunner(exec: PsExec = execFileSync as unknown as PsExec): (pid: number) => string | null {
|
|
186
|
+
return (pid: number) => {
|
|
187
|
+
try {
|
|
188
|
+
return exec(PS_BIN, ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8', timeout: PS_PROBE_TIMEOUT_MS })
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (psSaysNoSuchProcess(error)) return null
|
|
191
|
+
const e = (error ?? {}) as ExecFailureLike
|
|
192
|
+
throw new Error(`ps could not answer for pid ${pid} (status=${String(e.status ?? null)} signal=${String(e.signal ?? null)} code=${String(e.code ?? 'none')})`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The probe the fence routes use in production. */
|
|
198
|
+
export function defaultPidStartProbe(exec?: PsExec): (pid: number) => number | null {
|
|
199
|
+
return makePidStartProbe(psLstartRunner(exec))
|
|
200
|
+
}
|