@gotcos/glasses-server 6.99.33 → 7.0.7

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.
Files changed (76) hide show
  1. package/.env.example +18 -0
  2. package/CHANGELOG.md +417 -0
  3. package/README.md +3 -1
  4. package/managed-runtime-contract.json +3 -0
  5. package/package.json +1 -1
  6. package/server/index.ts +29 -7
  7. package/server/lib/agent-session-output-images.ts +627 -49
  8. package/server/lib/agent-session-store.ts +24 -11
  9. package/server/lib/agent-session-tool-images.ts +224 -0
  10. package/server/lib/agent-session-turns.ts +19 -2
  11. package/server/lib/api-auth.ts +6 -0
  12. package/server/lib/ask-work-plan.ts +3 -1
  13. package/server/lib/calendar-source.ts +491 -0
  14. package/server/lib/codex-bridge.ts +4 -1
  15. package/server/lib/codex-run-ledger.ts +29 -0
  16. package/server/lib/context-builder.ts +2 -1
  17. package/server/lib/drop-p2-runtime.ts +1 -1
  18. package/server/lib/drop-store.ts +10 -4
  19. package/server/lib/learning-needs-you.ts +24 -7
  20. package/server/lib/live-events.ts +533 -0
  21. package/server/lib/local-path-redact.ts +272 -0
  22. package/server/lib/meeting-ask-orientation.ts +40 -6
  23. package/server/lib/meeting-ask-runner.ts +30 -10
  24. package/server/lib/morning-brief-runtime.ts +4 -2
  25. package/server/lib/native-mac-caller.ts +29 -0
  26. package/server/lib/ping-engine.ts +1 -1
  27. package/server/lib/ping-sources.ts +11 -6
  28. package/server/lib/preview-content.ts +172 -0
  29. package/server/lib/preview-error-code.ts +10 -0
  30. package/server/lib/preview-folders-setting.ts +133 -0
  31. package/server/lib/preview-log-throttle.ts +18 -0
  32. package/server/lib/preview-thumbs.ts +139 -0
  33. package/server/lib/secret-path-guard.ts +80 -0
  34. package/server/lib/session-files-setting.ts +88 -0
  35. package/server/lib/session-links.ts +280 -0
  36. package/server/lib/thread-turn-queue.ts +10 -0
  37. package/server/lib/today-brief.ts +119 -28
  38. package/server/lib/web-memories-bridge.ts +3 -1
  39. package/server/lib/work-activity.ts +56 -6
  40. package/server/lib/work-session-activity.ts +260 -0
  41. package/server/lib/your-move-count.ts +9 -3
  42. package/server/routes/agent-session-bindings.ts +13 -0
  43. package/server/routes/agent-session-stream.ts +16 -0
  44. package/server/routes/agent-sessions.ts +146 -11
  45. package/server/routes/ask-work-plan.ts +4 -1
  46. package/server/routes/claude-sessions.ts +5 -1
  47. package/server/routes/health.ts +35 -0
  48. package/server/routes/live-events.ts +329 -0
  49. package/server/routes/permission-broker.ts +69 -5
  50. package/server/routes/session-preview.ts +200 -0
  51. package/server/routes/thread-turn-queue.ts +207 -3
  52. package/server/routes/today-brief.ts +44 -9
  53. package/server/routes/welcome-context.ts +23 -3
  54. package/server/routes/work-board.ts +9 -2
  55. package/server/web-control/app.css +40 -1
  56. package/server/web-control/app.js +1309 -56
  57. package/server/web-control/control.css +25 -0
  58. package/server/web-control/index.html +7 -2
  59. package/server/web-control/memory-review.js +100 -0
  60. package/server/web-control/panes/cos-chat.css +42 -3
  61. package/server/web-control/panes/cos-chat.js +267 -15
  62. package/server/web-control/panes/meetings.css +18 -2
  63. package/server/web-control/panes/meetings.js +24 -8
  64. package/server/web-control/panes/memories.css +17 -2
  65. package/server/web-control/panes/memories.js +127 -42
  66. package/server/web-control/panes/messages.js +21 -2
  67. package/server/web-control/panes/preview.js +318 -0
  68. package/server/web-control/panes/questions.js +28 -8
  69. package/server/web-control/panes/reader-media.js +409 -23
  70. package/server/web-control/panes/sessions.css +32 -3
  71. package/server/web-control/panes/settings.js +96 -5
  72. package/server/web-control/panes/today-actions.css +11 -0
  73. package/server/web-control/panes/today-actions.js +45 -12
  74. package/server/web-control/panes/work.css +10 -0
  75. package/server/web-control/panes/work.js +10 -2
  76. package/server/web-control/shell.js +17 -14
package/.env.example CHANGED
@@ -514,6 +514,7 @@ COS_CONTINUE_FULL_PERMISSIONS=1
514
514
  # COS_PIN_BOOT_REAP=1 # free a thread a crashed run held, 31 min after the restart and once its child is gone (default on; 0 = the 24 h reap)
515
515
  # COS_QUEUE_RESEND=1 # Send again on a refused or expired queued turn (default on)
516
516
  # COS_SESSION_STEER=0 # steer a busy Claude session through its peer inbox (default off)
517
+ # COS_STEER_MIDTURN=1 # a message parked at a busy Claude desk session lands at its next tool step through the peer inbox (default on; 0 = waits for the turn to end)
517
518
 
518
519
 
519
520
  # ── Answer with your own key (6.80.0, opt in) ──────────────────────────────────
@@ -523,3 +524,20 @@ COS_CONTINUE_FULL_PERMISSIONS=1
523
524
  # in data/key-secrets/ (0700). Your key is saved on this Mac: apps and agents that run on this
524
525
  # Mac as you can read it, so your provider's own spending limit is the real stop.
525
526
  # COS_KEY_LANES=0
527
+
528
+
529
+ # ── Files and images sessions make, on the web (6.99.35) ───────────────────────
530
+ # Control Web shows images and files a session saved in its project folder (Markdown images,
531
+ # and files a Claude session sent with SendUserFile). On by default; the switch is
532
+ # "Show files and images your sessions make" in Control Web Settings, Privacy (saved in
533
+ # data/web-session-files-settings.json). 0 / false / off forces it off and wins over Settings;
534
+ # no value forces it on.
535
+ # COS_WEB_SESSION_FILES=0
536
+
537
+
538
+ # ── Live state for Control Web (7.0.1) ──────────────────────────────────────────
539
+ # GET /api/events: one change feed per open Control Web page, so the pet, session rows, Today's
540
+ # counts and running clocks follow what is running now (docs/live-events-contract.md). Ids,
541
+ # states, counts and times only, never titles, words, file names or paths. On by default; read
542
+ # at start. 0 / false / off registers no route, and the page polls exactly as before.
543
+ # COS_WEB_LIVE_EVENTS=0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,420 @@
1
+ ## 7.0.7 - Answer lists never change what Markdown draws
2
+
3
+ 2026-11-08. 7.0.6 (f98dbbd, installed privately on Miles's Mac) plus four final QA fixes. Server and Control Web. This is the public candidate, paired with COS Control 0.5.380.
4
+
5
+ - **A Control Web tab left open across the update shows words, not raw card markers.** Link-card markers now go only to a page that asks for them (cards=1): the 7.0.7 Sessions reader and Today's session sheet. A tab opened before the update (Control Web has no reload-on-version), COS Control and any older client get each card's words, as 7.0.4 showed them; the cards themselves still arrive in outputLinks.
6
+ - **Answer lists never change what the reader draws (final QA W1).** 7.0.5 puts a COS answer's inline numbered list on its own lines, and a new line could start with a character the renderer reads as Markdown: "Note this. > 5 users said: 1. ..." drew "5 users said" as a quote and lost the ">", "Done. # of open items: 1. ..." drew a heading and lost the "#", and "**Steps: 1. ... 2. ...**" split the bold across lines so both "**" showed. Now a cut never enters inline code, `**bold**`, `__bold__`, `*italic*` or `_italic_`; a lead-in "Label:" that starts with `#`, `>`, `-`, `*`, `+`, `|`, `[`, `<` or a number marker stays on the line before it; and a list whose item starts with `#`, `>` or a task box (`[x]`) is left as it was written. The three QA answers now draw exactly the characters they drew before 7.0.5; only line breaks differ. Copy turn copies the answer as stored, as before.
7
+ - **Preview refusal logs are rate limited (final QA N1).** The preview routes' refusal lines go through the 7.0.4 routes' limiter: the same line logs once per 10 seconds, with the count it held back.
8
+ - **Notes corrected.** The 7.0.4 known limits now say the live stream's `prompt` events are redacted (0c8cb5e) and its other event kinds are not. The 7.0.5 note says exactly which Markdown leaves an answer alone.
9
+ - No other change from 7.0.6. Default-on for every install: card markers only on request (older tabs and clients see words), the answer list rules above (a narrowing of 7.0.5's) and the log limit.
10
+
11
+ ## 7.0.6 - The preview viewer always opens on screen
12
+
13
+ 2026-11-08. 7.0.5 (689b9e8, installed privately on Miles's Mac) plus one fix. Server and Control Web only. Pairs with the same COS Control as 7.0.5.
14
+
15
+ - **The viewer opens on the visible part of the page (Miles's Duo, IMG_0541 and IMG_0543).** Opening an image, file or preview could show only the viewer's bottom edge, because it filled the browser's whole layout area instead of what is on screen after a keyboard or a browser bar had moved it. It now takes the visible area, read the same way Today's session sheet reads it, and follows it while open. A layout note is logged when it opens or moves, so a device report has the numbers.
16
+ - No other change from 7.0.5. Default-on for every install: this fix only.
17
+
18
+ ## 7.0.5 - Preview engine phase 1 and the Today summary cap
19
+
20
+ 2026-11-08. On top of the final 7.0.4 (2104410). Server and Control Web. The COS Control Sessions pane reads the same routes (contract in the 7.0.5 preview CONTRACT.md); its Quick Look half ships with Control, not here.
21
+
22
+ **Default-on changes, for every install:**
23
+ - **Link cards, on through the existing switch.** When a session's reply links a local file (`[the plan](operations/PLAN.md)`, `[server.ts:42](server.ts:42)`, an absolute or `~/` path, a `file://` URL), the link becomes a card where it sits: the link's words, the file's type, size and line. Tapping it opens the file in the reader's viewer. They ride "Show session files, screenshots and images", which is on by default, so every install gets them. Turned off, the words stay and nothing else is sent: no id, no name, no count (the "N more linked files are not shown as cards." sentence goes too, also when the reply cap cut it).
24
+ - **Link paths are always redacted.** Each local link reaches the browser as its words plus an opaque card id (`[the plan](cos-file:0)`), with previews on or off, past the 20-card limit too (those are counted in words). A path-looking label becomes its base name. Web links and `#anchors` are left alone.
25
+ - **A card keeps its line.** A link whose words are the file name keeps the line it points at (`app.ts:42`, as 7.0.4 wrote it). COS Control's helper gets each card's words with no `cos-file` marker, because no Control build draws link cards yet.
26
+ - **One viewer for what a card or a sent file opens**, in the Sessions reader and in Today's session sheet: Markdown drawn with the reply renderer, text and code with line numbers and a jump to the linked line, CSV and TSV as a table (200 rows a page, 50 columns, 5,000 rows), HTML with scripts off, a PDF inline on a desktop and in its own tab on a phone or any touch screen (the unfolded Duo too), pictures through the normaliser. Office files, audio, video and folders still download or say "open it on your Mac" (phase 2).
27
+ - **HTML is shown full length** (up to 32 MB, was cut at 512 KB) under its own sandbox. Nothing on the page loads from the internet: the policy is sent as a header and also injected as the first tag of the page. "Run scripts" is a tap per open, off each time, in an opaque origin: scripts cannot fetch data, read COS, open windows or leave the frame, and the WebRTC constructors are removed before the page's own scripts run (the canary measured 5 UDP packets without that and 0 with it).
28
+ - **Thumbnails** for PDF and HTML cards come from Quick Look (`qlmanage -t`) on a private copy in a 0700 temp folder, two at a time, 10 s at most, cached on disk under the data folder (`preview-thumbs`, 128 MB and 7 days at most). An HTML page with no charset is read as UTF-8 for its thumbnail. No picture means a plain card, never a broken image.
29
+ - **Secret files stay refused for link cards too.** The 7.0.4 secret-name guard decides every card and every read, plus the names `hook-token`, `hubspot.config.yml` (or `.yaml`), `.mcp.json`, `.credentials.json` (Claude Code's sign-in) and `auth.json` inside a `.codex` folder. A linked file with another hard link is refused as a secret, as sent files are. A secret card says "Not shown: this file may hold secrets." and offers no button.
30
+ - **SendUserFile `files` sent as a JSON string** (11 of 1,112 real calls) is read like a list, with the same checks. Before, those files were dropped silently.
31
+ - **Today summary: up to 100 a day** (was 60). It is now a setting, "Summaries per day", 1 to 100, in Settings under the Today summary switch. The server enforces it, and the status line says "Today's N summaries are used."
32
+ - **Today summary after a reconnect.** When the sessions or Today read recovers from a failed one, the next summary ask starts at once instead of waiting out the 10-minute spacing. At most once per 10 minutes; "only when something changed", the 3-failure pause, the switch and the daily cap still apply.
33
+ - **COS answers read as lists on the web.** An answer written for the lens as one paragraph ("What it unlocks for me: 1. Prep... 2. Catching conflicts...") is drawn with each numbered item on its own line under its lead-in, in the COS chat card and the Messages reader. Only line breaks are added, on the web only: the lens and the stored answer are unchanged, and an answer that already has Markdown structure is left alone: a bulleted or numbered line, a heading, a quote or table line, a blank line or a code fence. (7.0.7 adds that a cut never enters inline code, bold or italic, and never starts a new line that the renderer would draw as a heading, quote, bullet or table.) The chat card also keeps an answer's own line breaks now (they ran together before).
34
+
35
+ **Off by default:**
36
+ - **Extra folders** (Settings, Privacy, under the session files switch). Empty until you add one. A session may open files only from its own project folder, plus each folder named there. Broad folders (home, Documents, Library, /, /Volumes, temp) and anything inside Library, .cos-glasses, .ssh, .codex, .claude or .config are refused with the reason ("COS never previews from that folder."). A folder saved before that rule stops counting.
37
+
38
+ **New routes** (under the usual /api gate; no request or answer carries a path):
39
+ - `GET /api/preview/agent-sessions/:provider/:sessionId/:id/meta` and `.../content?part=raw|inline|text|table|html|html-run|thumb`. A busy Mac answers 503 `previews_busy`; a secret 403 `secret_file_denied`; previews off 403 `previews_off` before anything is read.
40
+ - `GET|POST /api/preview-folders/settings`.
41
+ - `GET|PUT /api/today/brief/settings/cap` (`{ dailyCap }`, clamped to 1 to 100).
42
+
43
+ **Final QA fixes:**
44
+ - **Preview refusals and failures are logged** on the new preview routes, one line each: `[preview] meta|content refused reason=<code> provider=<provider>` plus the part, the file kind and a detail code where there is one (`error_ENOENT`, or the Quick Look reason: `timeout`, `exit`, `no_png`, `bad_png`). Never a path, a file name, an id or a session's words. A missing id is ordinary and is not logged, as on the 7.0.4 routes.
45
+ - **Summaries per day** on Control Web ignores an emptied field, letters, 0 or a negative: nothing is sent, the saved number comes back and the row says "Type a number from 1 to 100." Before, an emptied field saved 1 a day and said "Saved".
46
+ - **A Codex `view_image` with no readable source path is shown as unavailable, never served** (2104410, part of the final 7.0.4 and not in its notes). Its arguments must be JSON under 16 KB with a non-empty `path` (no NUL) for the secret-name guard to judge it; before, such an image was shown.
47
+
48
+ **Load:** the preview routes keep one reading of a transcript for 30 s while it is unchanged (20 cards are one parse, not 20), at most 2 preview reads and 2 conversions at once.
49
+
50
+ **Known limits:**
51
+ - A link inside a previewed Markdown file is shown as its words, not as a card (phase 2).
52
+ - A PDF on a phone opens in a new tab; inline PDF pages on a phone are phase 2.
53
+ - Not checked on a real iPhone Duo yet: the renders are headless Chrome at 458x667, 703x1000 and 1000x703 with touch, and 1440x900.
54
+
55
+ ## 7.0.4 - Session image previews, Today parity and week QA fixes
56
+
57
+ 2026-11-08. Matched candidate with COS Control 0.5.379. Includes the private 7.0.0 to 7.0.2 line; the public baseline remains 6.99.33 until publication receipts verify promotion. Control 0.5.377 and 0.5.378 keep their existing API contracts; the new native Today summary and calendar cancellation fixes require 0.5.379.
58
+
59
+ - Images attached to Claude/Codex prompts and returned by tools appear at their original session step, including after a growing history window. Refresh/unmount cancels obsolete thumbnails and requests. Existing Privacy controls still govern previews.
60
+ - Private Mac paths in list/search/detail prose become labels or base names for browser and remote readers; code and command fields keep their paths with the home folder shown as `[home]`; complete web URLs remain unchanged apart from a home folder in their query. Secret file names and realpath targets are denied across file/image routes, with honest permanent refusal copy.
61
+ - Includes calendar source-selection and declined-event fixes, memory review classification parity, and question retry/offline fixes.
62
+ - Ask Work: a session shows as fully unavailable only when neither Continue nor Fork works on it.
63
+ - Ask Work: the lock that blocks a second run and server maintenance is released once the server proves the stuck run has ended, instead of being held until a restart.
64
+ - No speaker naming threshold, enrollment, canonical recording repair or transcript correction is changed. Disabled live/cleanup flags remain disabled. Cursor prompt/tool image parity and physical Duo acceptance are not qualified by this cut.
65
+ - Detailed default changes, switches and limits follow. The historical 7.0.0 to 7.0.2 sections below describe the cumulative UI changes users receive from 6.99.33; no unrelated branch is implied to be included.
66
+
67
+ ### Session privacy and secret files
68
+
69
+ 2026-11-08. Integrated into 7.0.4. The server and Control Web reader share these changes.
70
+
71
+ **Default-on changes (no switch for either):**
72
+ - **S1: no private Mac path reaches a browser or a remote reader in a session's words.** The session detail (`GET /api/agent-sessions/:provider/:sessionId`, with or without `?turns=`), the session list and session search turn every private Mac path in every string they send into its base name, keeping a line anchor: `[app.ts](/Users/<name>/repo/app.ts#L42)` reads `app.ts:42`, Codex's `[server/app.ts](/abs/server/app.ts:42)` reads `server/app.ts:42`, and bare paths, `~/...`, `$HOME/...`, `file://` and editor (`vscode://file/...`) URLs read as their base names. A Markdown link to a local file becomes its words (or the base name when it has none). Private means a path under `/Users/<name>`, `/Volumes/<name>`, `/private/...` or `/var/folders/...` (any letter case, doubled slashes too), or a home shorthand (`~/`, `~name/` with a letter first, `$HOME/`, `${HOME}/`). System paths (`/dev/null`, `/usr/bin/env`, `/tmp/x`, `/opt/homebrew`, `/home`, `/run/start`), relative paths, site paths such as `/api/x`, numbers like `~80/100` or `~1/3`, and web URLs are left exactly as written; a home folder inside a web URL's query or fragment reads `~` and the rest of the URL is kept. This covers the reply, the digest, the first prompt, every turn's words and captions, file and image names, and any field added later (a walk over the whole answer, `lib/local-path-redact.ts`). A read-only scan of 14 days had found 111 Claude links and 826 Codex links carrying `/Users/...` into Control Web.
73
+ - **Code and commands keep their paths; only the home folder is hidden, as a visible marker.** In code spans, fenced code blocks, a waiting permission's command (`waiting_detail`) and `failure`, the path is kept and only `/Users/<name>` is replaced, by `[home]`: `rm -rf /Users/<name>/Documents` reads `rm -rf [home]/Documents`, never the harmless-looking `rm -rf Documents`. The result is a display of the command, not a runnable copy (a `~` was not used: inside quotes it does not expand). Control Web's Needs you line says so: "Wants to run (home folder hidden) rm -rf [home]/Documents". The approval card (`/api/session-questions`) is not redacted and shows the exact command.
74
+ - **Only COS Control's own helper on this Mac gets a user turn's words as written (W1).** Control (0.5.378 and 0.5.379, unchanged) recognises that a Work prompt it sent has arrived by comparing the opening letters and digits of its own prompt with the session's user turns, so a prompt that opened with a path never matched: no replies shown, Continue refused, a Cursor note stuck at queued. Only the user turns' text in `recent_turns` of the `?turns=` detail is returned original, and only to a privileged local client: a request on a loopback socket (127.0.0.1, ::1 or IPv4-mapped loopback) that passed the token gate with the `X-COS-Token` header and carries no proxy header (`X-Forwarded-*`, `Forwarded`, `X-Real-IP`, Tailscale Serve's) and nothing a browser sends (`Origin`, `Referer`, `Sec-Fetch-*`, a `Mozilla/` user agent). Every cookie session, browser fetch, phone, LAN, tailnet and proxied request gets the redacted text. The answer stays `private, no-store` and varies on every header the choice reads. No new field.
75
+ - **The live stream's prompt is redacted like the turn history (final QA).** The glasses find the turn on screen by matching the stream's prompt against the history's user turns. With the history redacted and the prompt not, a prompt holding a Mac path never matched, so the glasses refetched for 20 seconds and then showed the turn twice. Only `prompt` events change; other stream events and server-side readers are as before.
76
+ - **S2: one secret-name guard on every route that serves a local file.** `lib/secret-path-guard.ts` (`isDeniedSecretPath`) refuses, case-insensitively, a file whose BASE NAME is: any `.env*` (including `.envrc`), `*-key.json`, `service-account*.json`, `.cos-profile*`, an SSH key name (`id_rsa`, `id_ed25519`, `id_dsa.pub` and the like), `*.pem`, `*.p12`, `*.pfx`, `*.key`, `AuthKey_*.p8`, `credentials*`, `.git-credentials`, `token.json` or `tokens.json`, `secrets.yaml` or `secrets.yml`, `.netrc`, `.npmrc`, `.pypirc`, `.pgpass`, `.htpasswd`, or `*.keychain*`; anything inside a folder named exactly `.ssh`, `.gnupg` or `secrets`; and the exact pairs `.aws/credentials`, `.aws/config`, `.docker/config.json`, `.kube/config` and `gh/hosts.yml`. File-name patterns are never matched against folders, so a project under `credentials-portal/`, `.envoy/` or `id_verification/` keeps its previews. It checks the path as written and its canonical (realpath) file, so an innocently named symlink to a secret is refused too. A sent file with more than one hard link is refused as a secret (realpath cannot see a hard link), at listing and again on the open file. Wired into sent output files, sent output images, Markdown output images and tool images: a path-only Read, and a Read whose bytes are embedded, judged by the path it read (`~/Desktop/credentials.png` is refused). Images with no path (a screenshot, a prompt attachment) have nothing to judge. The token gate, the previews switch (`previews_off`) and the transcript re-authorization of the id still answer first; then a refused file answers **403 `secret_file_denied`**. In the session detail its card is `state: unavailable`, `reason: secret_file`, `notice: "Not shown: this file may hold secrets"`, with the base name only, no size and no path. Before this, a file inside the session's workspace was served whatever its name (a SendUserFile of the repo's `.env` was a downloadable card).
77
+ - **A busy Mac says it is busy.** When the Mac is already reading its limit of previews, the file and image routes answer **503 `previews_busy`** with `Retry-After: 2`, and Control Web says "Your Mac is busy with other previews. Try again in a moment." instead of "Check that your Mac is awake". A busy thumbnail retries by itself after about 1, 2, 4 and 8 seconds, then waits for Retry.
78
+ - **Preview refusals and failures are logged.** Each is one server log line with its kind and a reason code (`secret_file_denied`, `too_large`, `busy`, `unavailable`, `error_ENOENT` and so on), never a path or an error message (an fs message carries the path). A burst of one reason logs once per 10 seconds with the count held back.
79
+
80
+ **Known limits:**
81
+ - Control Web maps `secret_file` descriptors and `secret_file_denied` content refusals to "Not shown: this file may hold secrets." No Retry or incorrect Privacy-switch advice is offered.
82
+ - `*.key` also refuses Keynote decks (same extension), by design of the list.
83
+ - A folder name with spaces is followed only across capitalised words, and only when the continuation ends at a real file extension or a `/`; a lowercase one with spaces leaves its tail as plain words (no leading `/`, no user name).
84
+ - The guard is name-based: a COPY of a secret under an ordinary name is not caught. Images go through the JPEG normaliser, so a text secret cannot leave as an image.
85
+ - A `~name/` with a letter first is read as a home folder, so prose like "~half/most" loses "~half/".
86
+ - Any local process that holds the API token can read user turns as written; it can already read the transcripts themselves.
87
+ - The Claude sessions routes are not covered by S1 here. The live session stream (`agent-session-stream.ts`) is covered only for its `prompt` events (0c8cb5e, in 7.0.5): they are redacted like the turn history. Its other event kinds are sent as before.
88
+
89
+ **Tests (B1, W1, W5 and QA fixes):** local-path-redact (92: a must-not-change table of prose, code, shell lines, ratios, fractions, dates, versions and https links, including every string the first cut rewrote; a must-redact table of Mac paths with spaces, dot folders, quotes, backticks, `file://`, `:42`, Markdown bold, italic and blockquotes), agent-sessions-work-match (21, through the real token gate: the helper over 127.0.0.1 and ::1 matches; Origin, Referer, Sec-Fetch-*, a browser user agent, proxy headers, LAN and tailnet sources and cookie sessions get redacted text), secret-path-guard (48), the preview routes and Control Web. Every new guard was mutated on a green baseline and killed. Read-only replay over 16,886 recent assistant replies: the first cut changed 147 lines with no private path in them; this one changes 0 and leaves no `/Users/<name>`.
90
+
91
+ ### Images a session looked at
92
+
93
+ 2026-11-08. Integrated into 7.0.4, on 7.0.2 (f3aa80c). Server and Control Web. Miles, 11/08: the Claude app shows the image a Read of a PNG returned under its step ("Read", "1 of 2"); Control Web showed only words. 6.99.28 covered images written into reply text and 6.99.35 covered SendUserFile; images that came back inside tool results, and images attached to a prompt, were missing.
94
+
95
+ **Default-on changes (all behind the existing previews switch in Settings, Privacy, now named "Show session files, screenshots and images" and described as covering files and images sessions send, screenshots and images they open, and images attached to prompts; off means 403 `previews_off` and no image detail in the session):**
96
+ - **The Sessions reader and Today's session sheet show the images a session looked at.** A Read of an image, a browser or computer-use screenshot, any tool result image block, and Codex tool output images (`input_image`). Each is a small thumbnail at the step where it happened, with its tool and place: "Read", "Screenshot", "1 of 2". A step rides on the assistant turn it followed (the words before it); a reply that opened with tool calls shows its step above its words. Tap one to open it full size.
97
+ - **Images you attached to a prompt show on your YOU turn**, labelled "Attached" (Claude base64 image blocks, Codex `input_image`).
98
+ - **A Claude prompt that was only an image (sent through the SDK) now appears as a YOU turn reading "[Image]".** Before, it was not in the history at all. This also reaches the lens's scroll-up history (`?turns=`), which reads the same turns.
99
+ - **New route `GET /api/agent-sessions/:provider/:sessionId/tool-images/:imageId/content`.** Behind the same token or web cookie gate. The id is opaque (`ti_` and 64 hex), process-scoped, and must name an image in the session's newest 40 turns. Embedded bytes are decoded on the server and served as the type they ARE (PNG, JPEG, WebP or GIF, read from the bytes, never the declared type; anything else is refused), with `nosniff`, `private, no-store`, `Content-Security-Policy: sandbox; default-src 'none'` and same-origin only. Over 8 MB decoded is refused (413) without decoding. No path in the request or the response.
100
+ - **A Read that names a path and carries no bytes** is served only when its canonical file is inside the session's exact workspace, through the same read and JPEG normalizer as reply-text images. A link out of the workspace, a path outside it (gone or not) and a broad root read "Not available on this Mac"; a path inside that is gone reads the same.
101
+ - **Session detail turns gain `toolImages` and `toolImagesMore`.** Opaque ids, the tool label, the place in the step and a state; no bytes, names or paths. At most 6 thumbnails a turn; the rest are counted and shown as "+N more" and never get an id. A detail read holds no base64 past the line that parsed it; a content read keeps only the one image it names.
102
+ - **Thumbnails load lazily.** Nothing is fetched until a thumbnail is near the screen, two at a time, so a long session does not pull every image. Without IntersectionObserver they load as reply-text images do.
103
+ - **Back closes an enlarged image or file, and you land where you were.** Opening one adds a history entry on the same address; Back (or Escape, or Close) closes only the viewer. The page and Today's sheet keep their scroll position, the session stays open, and a half-typed note is untouched. This also applies to the 6.99.28 image viewer and the 6.99.35 file viewer, where Back used to leave the page or close Today's sheet.
104
+ - **Previews off** strips `toolImages` and `toolImagesMore` from the detail as it does reply-text images and files, and marks the turn, YOU turns included.
105
+
106
+ **Not changed:** reply-text images (6.99.28) and SendUserFile cards (6.99.35) project and serve as before; their shared workspace read moved into one function (`workspaceImage`) with the same checks.
107
+
108
+ **Known limits:**
109
+ - A step that never got words before your next prompt (the agent only ran tools, then you spoke) is not shown, so it is never pinned on you.
110
+ - Cursor transcripts are not covered.
111
+ - The renders are browser emulation (headless Chrome at 382x557 and 951x539), not physical Duo acceptance.
112
+
113
+ ### Calendar QA fixes
114
+
115
+ 2026-11-08. Integrated into 7.0.4 from `claude/calendar-qa-fixes`. Fixes W20 (server half), W21 and W23 from the week-wide QA of 1-8 Nov. The Python half is MU `operations/scripts/calendar_context.py` and `cos_api_bridge.py`, which the running server and the scheduled scripts read live from the MU working tree. The Swift half of W20 ships in COS Control 0.5.379.
116
+
117
+ **Default-on changes (only for anyone with a calendar snapshot from COS Control; `COS_CALENDAR_SNAPSHOT=0` turns all of it off):**
118
+ - **Connect with nothing picked is "not set up", never an empty day (W20).** A granted snapshot with zero calendars selected now reads as `unselected` (new `calendarStatus().snapshot` value), and every calendar read keeps the previous source, the bridge, until the first pick. Before, it was a fresh, final, empty day. With no bridge either, Today says "not connected".
119
+ - **One switch polarity in both readers (W21).** `COS_CALENDAR_SNAPSHOT` is on by default and only `0` (after trimming) turns it off, in the server and now also in MU `calendar_context.py`, which read the snapshot only when the switch was `1`. So with the switch unset, the scheduled scripts read the snapshot too. They use it only when it is fresh, granted, has a calendar picked and its window covers the request (about a day back and a week ahead); a wider request (`system_health_check` 14 days ahead, `weekly_planner` days back) keeps the old cache. A parity test runs one table against both readers.
120
+ - **Meeting Ask suggests a domain for a snapshot event (W21).** Snapshot rows carry `domain: null` because the classifier lives in Python people context. Meeting Ask now sends its candidates (at most 5: title, attendee names, calendar name; never an address or domain) to the bridge's new `calendar-classify` command over stdin, which applies the exact rule of the bridge's own calendar answer (`classify_event_by_attendees`). 3 s limit; no bridge, an older bridge or a bad answer leaves the match without a suggestion, as before. A row that already carries a domain is not asked again. The `with <person>` line in Ask context stays bridge-only.
121
+ - **Declined meetings and family travel are never Meeting Ask's match or Today's count (W23).** Meeting Ask runs the same filter as Next up and Today (`countsAsMeeting`: timed and not `prep_worthy: false`) before it matches the recording start. Today's `remainingCount` counts the same rows as its list.
122
+ - **A sturdier family rule (W23).** A family calendar matches by word (`Family`, `Ukaoma Family`, `Family calendar`), not only the exact title. A travel-shaped title (the bridge's own flight rule: "flight" or an airline code and number) with no attendees never becomes Next up, prep, Today or Meeting Ask on any calendar, and on a family or shared calendar never does even with attendees. A non-travel event with real attendees on a family calendar (a school meeting) still counts, and an attendee-less hold on your own calendar is unchanged. Snapshot rows' `ownership` uses the bridge's words: `family_shared`, then `unverified_flight`, then `miles`.
123
+
124
+ **Upgrading from 7.0.2:** nothing to do. `COS_CALENDAR_SNAPSHOT=0` in the server's environment or a scheduled job's plist restores the cache path for that process.
125
+
126
+ **Known limits:** the travel rule is the bridge's regex, so an attendee-less hold whose title has an airline code and a number (for example "as 2027") is skipped too. The desktop Today and Swift halves are in Control 0.5.379.
127
+
128
+ **Tests:** calendar-source (+10: W20 state, source and Today; the polarity table and its Python parity pin; travel, family, ownership and remainingCount), meeting-ask-orientation-calendar (new, 5: the prep filter, the classifier call, refusals, names only). MU test_calendar_context (+11). Red before the fix: 13 server, 7 Python. Mutants from a green baseline: 18 of 18 server and 13 of 13 Python killed, each landing exactly once. Smoke on a copy of the real snapshot (counts only): 9 of 24 calendars picked, 129 events, 0 change prep status under the new rule this week; an unpicked copy goes to the bridge; the scripts' snapshot answers today the same as the cache (1), and the classifier answers 20 real rows in about 0.1 s.
129
+
130
+ ### Integrated QA work (memory review status)
131
+
132
+ On 7.0.2 (f3aa80c). Ships in 7.0.4. Miles, 11/08 (IMG_0524, IMG_0525): "There are several suggestions that don't have a next step but are sitting in 'need review' category. We should only tag memories that need actual review with that status." Pairs with MU `learning_events.py` (d48285855), which produces the list.
133
+
134
+ **Default-on changes (no switch for any of them):**
135
+ - **Needs review means a decision only you can make, with a real next step.** One predicate, `server/web-control/memory-review.js`, decides it for Today's Memories count, the rail story and the Memories pane's To review count and list; the server imports the same file for the count it reports. It counts: a suggested rule at or over its 0.8 bar (approve, edit or dismiss), a memory that says what a named person said with no attribution, a decision or correction with no area, a governed memory waiting for acceptance or release, and a memory the nightly retention pass would quarantine (it only previews, so it waits for Accept or Quarantine). Contradictions count once they are produced.
136
+ - **Suggested rules under the bar move to Learning.** They cannot become a rule, so their only action was Dismiss. They now show under a calm Learning group at the bottom of To review, with their confidence ("Learning. Confidence 0.38, under the 0.8 bar for a rule"), no warning colour, no badge and no count. One that gathers evidence and reaches the bar moves to Needs you now on its own. An older `learning_events.py` that still lists them as `low_confidence` is read the same way.
137
+ - **A dismissed, accepted or quarantined item leaves To review.** It used to stay listed with "Change dismissed" and "Restore suggestion". The open reader keeps it, so Restore is still one tap away, and it stays in Recent learning as history.
138
+ - **Proposed changes from scheduled tasks are listed calmly.** They read "Suggested by a scheduled task" instead of "Needs your review", and are not counted (they never were).
139
+ - **The rail story's "Next 7 of N" counts only items that need review.** N was the To review list's length, which also held suggestions and proposals with nothing waiting. The bridge status gains `learningToReviewTotal`; the needs-you block keeps `learning`, `learning_count` and `bar`.
140
+ - **A Control Web tab opened before the update keeps working.** Its old page never loaded `memory-review.js`, so the Memories pane threw on its first render. The pane now loads it itself when it is missing, counts with the server's number until it arrives, and repaints.
141
+ - **"Rules, sources and outcome evidence" no longer sticks on "Working…".** Closing the sheet or switching tabs while a read was in flight left it busy for good, so Outcome evidence (and the other tabs) never loaded again until the page was reloaded. Every read now ends: an answer, an error line with Reload, or a 30 second timeout that says your Mac did not answer. With no runs recorded, Outcome evidence says "No outcome evidence yet."
142
+
143
+ **Live counts on Miles's Mac (read-only, 11/08 07:xx):** needs-you 51 to 47 (4 suggestions at 0.38, 0.38, 0.49 and 0.55 to Learning; 47 ready to quarantine stay). To review rows 68 to 63 (those 4 and the 0.51 suggestion dismissed from Control). The shown count stays 7 (the cap); the rail reads "Next 7 of 47" instead of "Next 7 of 68".
144
+
145
+ **COS Control (Mac):** its count comes from the same `learning_events.py needs-you`, so it follows without an update. Its bundled Memories page (`memories-app.js`) gets the same labels and Learning group in COS Control 0.5.379.
146
+
147
+ **Tests:** memory-review (27: the 19-case table mirrored from MU's `test_memory_review_state.py`, an older producer's block, page decisions, and parity across the server reader and bridge, the real rail story code and the real Memories pane on three live-shaped blocks), memories-web (47: 2 new and 2 rewritten for review, 3 new for the stuck sheet; all 7 fail on 7.0.2's pane), learning-needs-you (6). MU: `test_memory_review_state.py` (28). Mutation checks on a green baseline: 27 in the server (25 killed at first; the 2 survivors became a new assertion that kills one, and a close-time reset that was not load-bearing and was removed), 12 of 12 in MU. Renders (browser emulation, not device proof) at 382x557 and 951x539: `operations/personal/wk45_2026/memory_review/` in MU.
148
+
149
+ ### Questions on Control Web: a failed read says so, a question at the Mac keeps its words, and a Serve hop from the Mac to itself counts as the desk
150
+
151
+ From 7.0.2 (f3aa80c). Ships in 7.0.4. QA week of 11/01-11/08, findings W12 and W13. Server and Control Web only.
152
+
153
+ **Default-on changes (no switch for any of them):**
154
+ - **A failed questions read says so on Today's session sheet.** Before, a read that failed left no cards, and a waiting Claude session read "Waiting on your Mac's prompt" with no way to read again. Now, when the last read of `/api/session-questions` failed (no answer, a non-OK status, a cut body, or an unreadable capability) and no card is drawn, the sheet says "Couldn't load this session's questions, so one may be waiting here." with a Retry link that reads again at once. The Mac line comes back when a read lands. A Codex or Cursor session keeps its reply box; a Claude one does not (a reply does not answer its prompt). A card already drawn from an earlier read stays the answer.
155
+ - **A question at the Mac keeps the words Today showed.** When Today's Needs you row had the question's words and no card on the sheet draws them, the sheet shows them read-only above the Mac line (or the failed-read line), never twice.
156
+ - **`requestFromThisMac` counts a `tailscale serve` hop from the Mac to itself as the desk.** It is the Mac only when the socket is loopback, there is no `Forwarded` or `X-Real-IP`, every `X-Forwarded-*` and `Tailscale-*` header is one Serve sets, and `X-Forwarded-For` is exactly one IP that is loopback or one of the Mac's own Tailscale addresses (IPv4, or IPv6 in fd7a:115c:a1e0::/48, now read from the interfaces too). Serve sets that header itself (`Header.Set` in tailscale's `ipn/ipnlocal/serve.go`, read 2026-11-08), so a remote device's spoofed value does not survive; a list, a name or a padded pair is refused. **No visible change today:** Control Web refuses every proxied request (`isWebControlSocketAllowed`, review W4), so a Serve hop gets 401 before this check runs. It decides only once Control Web is admitted through Serve.
157
+
158
+ **Known limits:**
159
+ - **At-desk questions still have no words on the web.** With the desk in use the broker takes the `desk_active` fast path before it parses the question, and the session row's `waiting_detail` is the tool name (`toolTarget` has no AskUserQuestion branch). So the sheet keeps words only when the page saw them (a held card) or Today's row had them. Getting them for an at-desk question means the broker or the signal store keeping question text, a privacy decision left for Miles.
160
+ - The sheet tests run the real panes in a vm with a fake DOM; nothing here was rendered on the Duo.
161
+ - The step strip was already 44 px on touch screens (app.css `any-pointer: coarse`, 7.0.2), so it is unchanged.
162
+
163
+ **Tests:** session-questions-web (22, 3 new), today-actions (230, 10 new). 32 mutants on a green baseline of 252 tests, each landing exactly once and killed.
164
+
165
+ ## 7.0.2 - Control Web on the iPhone Duo: no blank band under the chat, a composer that fits above the keyboard, and failed Asks that say why
166
+
167
+ 2026-11-08. Private. On 7.0.1 FINAL (2accf69), which carries everything since public 7.0.0. Duo Build A, plus Codex's provider-limit fix (605ca92), from the 11/07 Duo design audit. Server and Control Web only; no glasses, phone or Control update is needed. Sized from what the Duo itself reported in Comet: 382x557 folded, 951x539 unfolded.
168
+
169
+ **Default-on changes (no switch for any of them):**
170
+ - **No blank band under the COS chat after the keyboard closes.** The chat route (`#/messages/cos`) no longer keeps the page's 96 px bottom padding (40 px from 600 px wide), room for a bottom bar that was removed long ago. When the keyboard closes, the page scrolls back inside its range and, if the chat box had the keyboard, the composer returns to the bottom of the screen. WebKit render at 382x557: the band under the composer went from 96 px to 0.
171
+ - **The Model menu never widens the page.** A long model name made the folded page 455 px wide on a 382 px screen. It now shortens with an ellipsis.
172
+ - **The keyboard is seen in browsers that shrink the page for it.** Some web views (Comet and Chrome on iOS may) shrink the page itself when the keyboard opens, and Control Web then measured nothing covered. On a touch screen, while a field you type in has focus, the page height is now the tallest seen at that width, so the keyboard is measured either way. That height never goes more than 20 px past the last height read with nothing focused, so a browser bar coming back while you type is not taken for a keyboard, and a field focused again while the keyboard is still closing starts from the full height. With nothing focused, on a Mac or another fine pointer, or with a picker (a select) open, the reading is exactly 7.0.0's. Today's session sheet and the question card on Today use the same reading.
173
+ - **A page-wide keyboard class, used only by the chat.** While an on-screen keyboard is up, `<html>` carries `kb-up`, and the page publishes the visible area as `--vv-top`, `--vv-h` and `--vv-below`. Nothing restyles on `kb-up` alone: the chat column docks only on `kb-chat` (the keyboard is for a field in the chat), and only the composer that has the keyboard (`.cc-kb`) turns compact. The Messages search box and other panes are unchanged.
174
+ - **With the keyboard up, the chat composer stays above it.** Folded, it docks above the keyboard and turns compact: the box (3 lines at most), then two rows, the clip with Ask and Drop, then Model and Effort side by side. Measured in WebKit at 382 px with an empty box: 171 px (was 252). The camera button and the fine print come back when the keyboard closes. Unfolded (two columns), the chat column fits the space above the keyboard, follows it as it rises, and its header drops to one line; folding to one column with the keyboard up drops that fit, and after a fold or unfold with the keyboard still up the composer is brought back into view. The unfolded chat column also never asks for more height than the screen has (its 420 px floor is capped at the screen's height), and in its card the message list gives way before the composer does, so the composer and its fine print always fit (at 951x539 with the 44 px floor the composer had been pushed 21 px out of the card and its fine print clipped).
175
+ - **A failed Ask says why.** The card line now follows the server's error: out of usage (with the reset time when the provider gives one, for example "Codex is out of usage until Nov 13th, 2026 12:20 PM. Pick another model under Model and ask again."; an ISO or UTC time is shown in your own time zone), not signed in, the conversation too long for the model, too slow, or COS restarted or shut down on the Mac. Anything else keeps "COS could not finish this answer. Ask again when you are ready."
176
+ - **A Codex usage limit is kept as a usage limit.** Codex's own `usage_limit_exceeded` now reaches the job as `provider_limit` with a safe retry date, so the card above can say so and COS can route around an exhausted Codex. The raw provider error is never shown.
177
+ - **Ask again on a failed card.** It sends the same words and files as a new Ask, with the model and effort picked now, to the same conversation as the first try: the same COS thread, the chat's same session, or a Reply's own turn. It checks that before it starts, and again once the resend has worked out which conversation it will go to, immediately before it is sent, so a thread or session that changes in between is caught too. If that conversation has moved on (New thread, another thread now current, the chat on another session, thread mode switched), nothing is sent and the button stays, disabled, with "This conversation has moved on. Copy your words into a new Ask." It shows once per failed card, only for an Ask the Mac took, and never on a Prep card (Prep has its own Run). If its file can no longer be read, the line says to attach it again.
178
+ - **A Reply with no conversation of its own starts one.** Its card always said so ("Starts a conversation"), but it took the chat's session; it no longer does.
179
+ - **"Default" in the Model menu says what it is**, for example "Default (Codex Frontier)" on a Mac with `COS_G2_DEFAULT_MODEL=codex-frontier`, or "Default (key profile)" for a conversation bound to a key profile. `/api/models` gains `defaultModel` (`preference` and `source`: session, profile, env or built-in), with `?sessionId=` adding that session's own preference. The menu reads it again when the chat's conversation changes (in thread mode, each time the COS thread is read, a turn is recorded or New thread starts one) and after each Ask finishes. Only the newest read counts: a slow answer about an older conversation never replaces a newer label. An older server sends no field and the menu reads plain "Default".
180
+ - **Short touch screens get the phone layout.** A touch screen 720 px tall or less, at any width, counts as a phone: the unfolded Duo, and also short tablets and phones held in landscape. They get the 44 px tap floor and the phone Ask sheet (full width from the top) instead of the centred desktop card, and Work's board shows 8 cards per column before "Show all" (was 40). The rail, its story lines and hot dots stay wherever the rail shows (600 px wide and up). A Mac or a tall tablet is unchanged.
181
+ - **Layout reports from every pane** go to the existing client diagnostics log (`POST /api/diag/client`, `web.layout`): the device once per load after sign-in, each focus of a typing field on a touch screen, every keyboard open and close (numbered, so none is dropped as a repeat), and the gap and scroll range about 300 ms after the keyboard closes. Numbers and fixed labels only, never text, ids or the user agent. Budgets: 80 a page load, of which focus lines have their own 20 and everything else the other 60, so typing never uses up the keyboard reports; 20 in any 10 s (the server allows 30), with focus lines held to half of that. Today's sheet also reports every keyboard open and close, not only when its layout changes; its `wide` field still means 600 px wide or more, and a new `shortTouch` says when a short touch screen counts as a phone.
182
+
183
+ **Upgrading from 7.0.0:** nothing to do.
184
+
185
+ **Known limits:**
186
+ - Not yet accepted on the physical Duo. The checks are vm tests and a WebKit render (Playwright's macOS WebKit) at 382x557 and 951x539 with the page itself shrinking for the keyboard. The other keyboard behaviour (only the visible area shrinks, iOS Safari's) and focus zoom need iOS.
187
+ - Comet reports as `crios` in the layout reports: Comet and Chrome on iOS cannot be told apart.
188
+ - The "Default (...)" label is read again on a conversation change and after each Ask; a change made elsewhere (another device, `.env`) shows after the next Ask. In a Reply composer under a message it reflects the chat's conversation, not the replied-to turn's.
189
+
190
+ **Tests:** duo-build-a-web (73), today-actions (220, of which 4 new: 3 keyboard cases with a shrinking page, one for every open and close), duo-layout-630-web (45, the 10 s and page-load budgets), health-default-model (6). 86 mutants on a green baseline of 344 tests, each landing exactly once and killed by a named test.
191
+
192
+ ## 7.0.1 - Control Web live state: the pet, Today and Sessions follow your sessions as they change, Today Refresh, and Needs you clears when a session moves on
193
+
194
+ 2026-11-08. Private. On public 7.0.0 final (68fb077) plus a test-only isolation fix (ef71c9c), the live state server half (`claude/live-state-server`, 8a65a72), the live state web half (`claude/live-state-web`, 0a70721) and Needs you stale (`claude/needs-you-stale`, c9ba6e9). Miles, 11/07: the pet shows what was running at the last fetch, not now; session activity, replies and running time should be accurate while caching keeps things fast; and Today's Needs you kept a Work row ("COS Glasses - QA review before publish, Oct 31, Waiting for your reply in Personal") while he worked in that session all day.
195
+
196
+ **Private 7.0.0 on Miles's Mac was 39d063e**, built before the independent QA fixes. So 7.0.1 is also the first private build that carries the 68fb077 QA fixes; they are the last group in the list below (already in public 7.0.0).
197
+
198
+ **Works with COS Control 0.5.377 and later.** Server and Control Web only; no glasses, phone or COS Control update is needed. No route or response field was removed.
199
+
200
+ **Default-on changes (everything a 7.0.0 user gets without touching a setting):**
201
+
202
+ *Live events on the server (`COS_WEB_LIVE_EVENTS=0` registers no route and keeps 7.0.0's polling):*
203
+ - **New route `GET /api/events`.** One server-sent events stream per open Control Web page, behind the same token or web cookie gate as every other route. It sends a snapshot on connect, then a frame whenever the server's own session state changes: a session added or ended, working, waiting or idle, a reply landed, a question opened or closed, the queue moved. Each session carries `startedAt` and `turnStartedAt`, so the page can tick running time itself. Ids, states, counts and times only, never titles, words, file names or paths. Contract: `docs/live-events-contract.md`.
204
+ - **While at least one stream is open, the server reads the Claude session registry, the questions list and the queue folder every 2 s** to notice changes that have no signal of their own. Measured on a scratch server with this Mac's 76 registry files and no hook traffic: about 0.27 % of one CPU core for the first stream, about 0.003 % for each more, no memory growth. With no stream open nothing is read. Hook events (prompt, permission prompt, Stop, SessionEnd) reach an open page in about 0.1 to 0.25 s; a session's end in up to about 2 s.
205
+ - **A hook event reads no file.** It rebuilds the hook-derived states from memory, against the registry as the last 2 s read saw it; the registry, the questions list and the queue folder are read only on the 2 s tick. (The first build re-read all three on every hook event, up to about 6 times a second with many busy sessions; the CPU under heavy hook traffic has still not been measured.) A brand-new Claude tab can take up to 2 s to appear.
206
+ - **At 16 open streams the oldest is closed to admit a new one**, instead of refusing the newest (which is usually the page just opened, while the oldest may be a phone that dropped off Tailscale). Each eviction, refusal and slow-client drop is logged with the open count. A closed page reconnects with its last id.
207
+ - **A registry folder that exists but cannot be read** keeps the last good state and is marked degraded, instead of reading as "nothing alive" and ending every Claude tab. A missing folder is still empty.
208
+
209
+ *Control Web live state (with a server that has `/api/events`; `COS_WEB_LIVE_EVENTS=0` on the server, or a server older than 7.0.1, gives exactly the 7.0.0 page):*
210
+ - **One live stream per page.** Control Web opens one `EventSource('/api/events')` per tab and keeps it while the tab is visible. The pet's count and caption, Today's Running rows and counts, Needs you, and the Sessions rows' state chips follow it within about a second, instead of showing the last fetch. Cached data still paints first; the stream corrects it.
211
+ - **Each row joins its own live session by its full id.** The 8-character prefix is used only when one side is exactly 8 characters (a Claude tab the list still names by its short registry id) and only one session matches. Codex ids are time-ordered (UUIDv7), so parallel Codex sessions started within about a minute share their first 8 characters; they stay separate rows with their own states.
212
+ - **For Codex and Cursor, the session list decides open or closed.** The stream moves only working, waiting or idle and the times for those sessions. (The stream's idea of "open" for them is a 30-minute hook window, while the list checks the process.) Claude rows still take open and closed from the stream.
213
+ - **A new session counts at once.** When the stream reports a session the list has not shown yet, a placeholder row ("New Claude Code session") joins the counts until the list read it triggers brings the title. A session that starts or ends triggers one list read (debounced to 1 s) for the words. A reply triggers the same read at most once every 12 s, so many sessions finishing turns do not poll the list every second.
214
+ - **Running time ticks every second** on Today's Running rows ("Working for 4m 12s"), from the turn's start on the Mac's clock, with no polling. It pauses while the page is hidden.
215
+ - **Reconnecting.** No frame for 30 s, or a dropped stream: Today's header says "Reconnecting", and the session list is read every 5 s until the stream is back. Those reads are shown as read: the last live state is not laid over them, and placeholder rows go, so a session that finished while the stream was down shows as finished. The same holds while the server is full ("busy") and while the tab is hidden. The stream resumes from its last id (`?after=<epoch>.<seq>`), so nothing in between is missed, and live corrections start again with its first frame.
216
+ - **Coming back** to the tab, from the back-forward cache, or back online: the list is read at once and the stream resumes from its last id. A hidden tab holds no stream and runs no live timer (not the watchdog, the list read, the retry or the 1 s clock).
217
+ - **Counts say how old they are.** While the page is not live, counts older than 30 s show their age beside Refresh and in the Sessions header ("As of 2 min ago"). Live counts show nothing extra.
218
+ - **Questions.** A change in the server's question set asks the question cards to read at once; the cards' own poll is unchanged and stays the fallback.
219
+ - **Fewer summary asks while live.** With the stream on, Today repaints on every session change, so it asks for a new Today summary at most once a minute (the server still decides). Without the stream, the 7.0.0 rule is unchanged.
220
+
221
+ *Today summary and Refresh (no switch; the summary itself stays off by default):*
222
+ - **The Today summary is written by Sonnet, not Haiku** (only when the summary is switched on). Measured 11/07 with a full-size prompt: about $0.016 to $0.018 a call, against $0.0008 for Haiku. The 60 a day cap and the 10-minute interval are unchanged, so the worst day is about $1.10 where it was about $0.05. On a Claude subscription the CLI draws plan usage; these are the CLI's own estimates at API prices.
223
+ - **`POST /api/today/brief` accepts `"force": true`**, for the Refresh button. It skips the 10-minute interval and the "nothing changed" check, at most once every 30 s, inside the same 60 a day, breaker, switch, recording and drain gates. A throttled refresh answers the current summary with reason `refresh_throttled` and `retryAfterMs`. Without `force`, nothing changes.
224
+ - **Refresh** under Today's date (44 px target). It reads every Today source again (sessions, Work, the welcome, the recording, voices and memories), then asks the server for a new summary with `force: true`. The line beside it says "Updating", then "Updated just now". With the Today summary switched off, Refresh reads the state only. Screen readers hear the line only when its state changes (Updating, Updated, Reconnecting, a limit), not every second of a countdown or an age.
225
+ - **The server's limits are said plainly:** "Try again in 20 s" (one refresh per 30 s, counting down), "Daily summary limit reached", or the summary's other pause reasons.
226
+ - **Settings, Today summary** now says Sonnet writes the summary, and that Refresh asks for a new one at once, at most every 30 seconds.
227
+ - Against a 7.0.0 server Refresh still works: the state is read again, and when that server refuses `force` (400 `invalid_today_brief`) the summary is asked for the ordinary way.
228
+
229
+ *Needs you clears when a session moves on (no switch):*
230
+ - **A needs-input or blocked report stops counting once you start a turn in its session after it.** `GET /api/work-board/activity/open` reads, for each session it would list as waiting, the newest turn you started: in Claude a real prompt (not a background agent's task notification, a tool result, a message from another session, a system reminder, a skill or command body, or a compaction summary); in Codex a message you sent (not injected context). A turn of yours more than a minute after the report supersedes it: the item is grouped as if no report stood (usually running), and carries `supersededAt`. The session's own work never supersedes a report (the first build counted any activity, so a background agent finishing could clear a question you still owed). The report stays in `progress` as COS Control recorded it. A session it cannot find or read keeps its report, as before. Checked on 11/08: the Oct 31 row's session has a turn of yours at 11/08 04:49 UTC, so it still clears.
231
+ - **The read is bounded and shared.** At most 12 sessions per read, the stalest waiting reports first; the transcript index is built once per read; each session's answer is reused for 30 s; concurrent callers (Today, the badge recount) share one read; and the 2 s limit stops the walk itself, not only the wait. Measured cold on this Mac: about 1.4 s for one Claude session (index of every transcript plus a 1.3 MB tail); cached: 0 ms.
232
+ - **Control Web Today leaves a superseded item out of Needs you when it regrouped to running**, so its count, headline and rail line drop it too. A superseded completed handoff regroups to "Needs a look" and stays, as the badge counts it.
233
+ - **The inbox summary's `waiting` count (the Messages badge) counts the same items Today shows as waiting**: a superseded item that regrouped to running leaves both; one that regrouped to "Needs a look" stays in both. (Today still also lists running handoffs, "Handed to you", which the badge leaves out.)
234
+ - **A waiting Work row on Today is dated by when it started waiting** (the new `waitingSince` field: the newest progress event of the report's own kind, or the newest Jev reading for a Jev report), not by the card's newest event.
235
+
236
+ *New on a Mac that ran the private 7.0.0 (39d063e): the 7.0.0 QA fixes (68fb077, already in public 7.0.0; details in the 7.0.0 section):*
237
+ - **The session files switch fails closed.** A saved setting that is damaged, unreadable or not this format reads as off (`source: 'damaged'`), and Settings, Privacy says so; only a missing file is first use and on.
238
+ - **The composer no longer promises "next step" for a message with files.** A busy Claude session reads "Working right now. A message with files waits and lands when this turn ends."
239
+ - **Calendar dedupe merges only the same meeting** (with a calendar snapshot): same external id and occurrence, or same title, start and end plus a shared named attendee or the same meeting link.
240
+ - **Today's "Later" is not crowded out** by all-day and non-prep events.
241
+ - **Tomorrow's count is a calendar day** in local time, so a DST day counts the right events.
242
+ - **A file card that is unavailable for now** says "Not available right now. Reopen the session to check again." with no Retry promise.
243
+
244
+ **New fields (additive, optional):** `waitingSince` on every Work activity with a needs-input or blocked report, `supersededAt` on open-list items, `reason: refresh_throttled` with `retryAfterMs` on a throttled forced brief. No route or field was removed.
245
+
246
+ **Limits:**
247
+ - A Codex or Cursor session whose hooks never reached this server has no change signal without reading its transcript, so the stream does not carry it; it still shows through the session list, as in 7.0.0.
248
+ - A Claude tab with no hooks reads the registry's own status (busy, waiting, idle), and its turn clock is when that status last moved.
249
+ - `replies` counts turns that ended since the server started, not a lifetime count.
250
+ - Streams are capped at 16 per server; a client that stops reading is dropped at 256 KB unsent and resumes with its last id. A full server (503 `live_events_capacity`) is retried after 30 s with no fast polling.
251
+ - Every pane behaves as in 7.0.0 when the stream is absent (404, 401, 403 or a non-stream answer). With no stream, the page tries once more only when it comes back to the screen ten or more minutes later.
252
+ - With more than 16 live pages open at once, the oldest is closed for each new one, and the closed page reconnects, so pages take turns rather than one being refused.
253
+ - A peer that vanishes (a phone off Wi-Fi) holds its slot until TCP gives up or the cap evicts it; there is no per-stream age limit or heartbeat drain check.
254
+ - The browser's own reconnect resends the original `?after=` URL, and the query wins over the newer `Last-Event-ID`, so it replays frames the page already has (skipped by seq) or gets a snapshot once that id has left the 512-frame ring. A malformed `?after` falls through to the header.
255
+ - A page that detaches while the first stream is priming can leave the next snapshot up to one tick (2 s) old.
256
+ - `replies` can restart at 1 for a session whose counter was evicted past 800 sessions.
257
+ - `degraded.questions` and `degraded.queue` are never true today: the broker and queue reads swallow their errors.
258
+ - Without `COS_CLAUDE_SESSIONS_ENABLED=1` (on, on Miles's Mac) the registry is not read: registry-only Claude tabs are invisible and a tab's end depends on its SessionEnd hook.
259
+ - On the direct HTTP/1.1 listeners each open page's stream uses one of the browser's 6 connections per origin, shared across tabs.
260
+ - Ended COS child sessions enter and leave the live model, each costing every open page one debounced list read.
261
+ - Needs you: a Cursor session has no record clock or origin, so its report clears on any write to its transcript, including the agent's own.
262
+ - Needs you: a message that reaches the session through the cross-session door (a G2 dictation, another session's note) is recorded as a peer message, not a prompt of yours, so it does not supersede a report. Only a prompt typed in the session does.
263
+ - Needs you: a report that is repeated word for word adds no new event in COS Control, so its wait date stays at the first one; a prompt of yours after that date clears it even if the session then repeats the ask. It returns only when COS Control records a changed report.
264
+ - Refresh re-reads every Today source on each press, including during the "Try again in N s" countdown; only the summary ask is throttled.
265
+ - No install step yet probes `/api/events` on the real server; the route's mount is pinned by a source test, not a boot.
266
+
267
+ **Not changed:** COS Control's own Work card still shows the report it recorded; nothing writes the Work journal. There is no Dismiss on a Needs you row: the journal is COS Control's, and the server has no clean way to record one. COS Control's Today on the Mac does not show this row at all (its Today reads only `kind`/`action` items from `/api/work-board/your-move`, which this server does not send). Control Web Today still lists running handoffs in Needs you ("Handed to you"), which the inbox count leaves out.
268
+
269
+ **Why the Needs you row stayed (11/07).** The row was a Work handoff (receipt e840cc8e, card a3e811fbc87a on the Personal board, sent 10/30 21:13 UTC into the session as a Continue). COS Control's tracker takes a needs-input report from the session's status line for that card, and the last one it saw was at 10/31 07:25:59 UTC. No later turn mentioned the card again, so nothing replaced the report, and the open list kept the card in needsInput for the whole 14-day tracking window. It was not a queued app note: `waitingInApp` was false. "Oct 31" was the card's newest progress event (that same report), not its creation (10/30).
270
+
271
+ **Upgrading from 7.0.0:** nothing to do. `COS_WEB_LIVE_EVENTS=0` keeps 7.0.0's polling and page. The Sonnet change applies only with the summary switched on. The Needs you and Refresh changes have no switch.
272
+
273
+ **Tests:** `server/lib/live-events.test.ts` and `server/routes/live-events.test.ts` (seq monotonic, resume, snapshot on a gap, heartbeat, privacy allowlist, auth refused, cap, slow-client drop, switch off means no route, the list's own state derivation), the Today brief suites (Sonnet pin, forced refresh throttle, cap, breaker, switch, persisted floor), live-state-web (one stream per page, placeholder rows, the ticking clock, Reconnecting and resume, ages, Refresh, no stream means the 7.0.0 page), work-activity (the live receipt's times: the wait date, the supersede, the one-minute grace, blocked, done, the linked session only, the floor without a wait time, a completed handoff), work-session-activity (the session read, ids it refuses, the test-runtime guard, the 12-session cap, the Cursor clock, the 2 s bound, the route and the inbox count), needs-you-stale-web (app.js's own needsModel and needRowHtml on the live item), and the test isolation canary (ef71c9c). Review fixes (skeptic and Ghost Hunter, 11/08): red first on 675d39e, then each new guard mutated from a green baseline and killed; see the review fixes commit. Live state: 31 mutations, each killed from a green baseline; live canary on a scratch server: hook to frame 103 to 232 ms over http and https, resume from `Last-Event-ID`, a foreign-origin cookie refused. Needs you stale: red on 7.0.0, green here; each guard mutated once and killed.
274
+
275
+ ## 7.0.0 - Steer a busy session mid-turn, see the files your sessions send, Duo tables, resizable panes, and calendars from macOS Calendar
276
+
277
+ 2026-11-07. On 6.99.35 (61424472) plus the calendar attendee fix (d7558c7). The first public build after 6.99.33: npm goes from 6.99.33 straight to 7.0.0, and 6.99.34 and 6.99.35 (private builds, below) reach everyone here. Miles chose the major number for this release. No route or response field was removed and every client reads the server version by number, so nothing that works with 6.99.33 needs a change for 7.0.0.
278
+
279
+ **Works with COS Control 0.5.377 and later.** Server and Control Web only; no glasses, phone or Control update is needed. Calendars need a COS Control that writes the calendar snapshot (0.5.378 and later); with 0.5.377 there is no snapshot, and every calendar read behaves as in 6.99.33.
280
+
281
+ **Default-on changes (everything a 6.99.33 user gets without touching a setting):**
282
+
283
+ *Steering a busy Claude session (`COS_STEER_MIDTURN=0` turns the steering off):*
284
+ - **A message sent to a busy Claude session lands inside its running turn, at its next tool step.** Before, a message from Control Web, the phone or the glasses waited for the turn to end. Now it goes in through the session's peer inbox and is read at its next tool step; the row reads "Sent into the running turn." Same for Claude Code in the desktop app and in a terminal, and for glasses messages. Codex and Cursor sessions, a message with files, and a Claude session that is held (a COS child on the thread, a permission prompt, a fence, a cancel hold, no peer inbox) still wait for the turn end, as before.
285
+ - **Control Web says where a message lands.** With the switch on, the composer reads "Working right now. Your message lands at its next step." and, after a send, "The session is busy. Yours lands at its next step." With it off, the old turn-end lines. The busy answer gains `midTurn` and the park answer gains `landsAt`, only with the switch on; the park answer is still 202 `queued: true`.
286
+ - **A queued message COS cannot confirm reads "May have been sent", not "Not sent"** (no switch). In the Sessions reader's queue, Today's sheet and the Sessions row chip, a row whose delivery is unknown (a steer it could not confirm, a delivery cut off by a restart) says "May not have been sent. Read it, then say it again." A message refused for tampering still reads "Not sent".
287
+
288
+ *Files and images your sessions make (Settings, Privacy, "Show files and images your sessions make"; `COS_WEB_SESSION_FILES=0` forces it off):*
289
+ - **Files a Claude session sent with its SendUserFile tool show in the Sessions reader and Today's session sheet**, where they happened in the conversation. Images show as thumbnails. A PDF opens in a new tab; text, Markdown, CSV, JSON, YAML, XML and SVG open as words; an HTML page shows in a sandboxed frame with scripts off; anything else downloads as a plain file. Only a file's base name reaches the browser, never a Mac path. A file outside the session's workspace, or gone from disk, reads "Not available on this Mac".
290
+ - **The new switch is on by default** and also covers the images sessions make (6.99.28). Off, the image and file routes answer 403 `previews_off` before reading anything, and each message with images or files shows one line saying previews are off, with no names sent.
291
+ - **New read-only route** `GET /api/agent-sessions/:provider/:sessionId/output-files/:fileId/content`, behind the same token gate, re-checked against the session's newest 40 turns and its workspace on every read, 32 MB at most.
292
+
293
+ *Tables and panes in Control Web (no switch):*
294
+ - **Table words stay whole.** Cells wrap at spaces only, so a column is never narrower than its longest word; only a token of 25 or more characters (a URL, a hash) may break inside. A short first column stays on one line. A table too wide for its box scrolls inside the box with the first column held, and the page never scrolls sideways. This applies to every markdown table in Control Web: the Sessions reader, Today's session sheet, Meetings notes and Ask, and Memories answers.
295
+ - **Drag the line between two columns** in Sessions and Memories (760 px and up), Messages and Meetings (860 px and up) and Work (900 px and up), with finger, mouse, pen or arrow keys. A double tap resets it, and each width is remembered on that device. Sessions and Memories also get **Full width / Show list** in the reader's header. Default layouts are unchanged, and below each breakpoint (phones, the folded Duo) nothing changes.
296
+
297
+ *Calendars from macOS Calendar (no change until COS Control writes a snapshot; `COS_CALENDAR_SNAPSHOT=0` ignores it):*
298
+ - **Control Web Today asks `GET /api/calendar/today` once per paint.** With no snapshot it answers "not connected" and Today shows nothing new.
299
+ - **`/api/models` gains a `calendar` field** (source, age, access, counts; never titles). Not on public `/api/health`.
300
+ - With a snapshot from COS Control 0.5.378 or later: one calendar source for Next up, prep and Today, selected calendars only, declined, all-day, holiday, birthday and attendee-less shared events skipped, and prep tied to the meeting's occurrence (see 6.99.34 below).
301
+ - **An attendee name that is an email address is dropped and the domain kept.** macOS fills a participant's name with the address when there is no display name (124 such names on Miles's Mac). Names and domains only, as the calendar contract says. Same rule as COS Control's own reader.
302
+
303
+ *Fixes from the independent QA of the 7.0.0 candidate (Codex root-user, root-skeptic, root-completionist):*
304
+ - **The session files switch fails closed** (default-on change for anyone whose saved setting cannot be read). Only a missing file is first use and on. A saved file that is damaged, unreadable or not this format now reads as off (`source: 'damaged'`), because it may have said off. Before, it read as "nothing saved" and previews came back on. Settings, Privacy says "Couldn't read the saved setting, so previews are off. Turn it on to save it again." and the toggle saves a good file.
305
+ - **The composer no longer promises "next step" for a message with files.** With a file in the Sessions reader's box or Today's sheet, a busy Claude session that takes steering reads "Working right now. A message with files waits and lands when this turn ends." The note repaints as files are added or removed. The server already answered `landsAt: turn_end` for a message with files; the line after a send follows that answer.
306
+ - **Calendar dedupe merges only the same meeting** (default-on change with a calendar snapshot). Two events on different calendars merge when their external id and occurrence match, or when title, start and end match AND they share a named attendee or the same meeting link (query and fragment ignored). Before, the fallback keyed on the set of attendee domains, so two different meetings at one time with different people from one company merged into one. Two events on one calendar still never merge.
307
+ - **Today's "Later" is not crowded out.** `GET /api/calendar/today` drops all-day and non-prep events (declined, holidays, attendee-less shared events) before it takes the next 3, so a day of all-day events no longer leaves Later empty while timed meetings remain.
308
+ - **Tomorrow's count is a calendar day** in local time, so a 23 or 25 hour DST day counts the right events.
309
+ - **A file card that is unavailable for now** says "Not available right now. Reopen the session to check again." It has no Retry button, so it no longer says Retry.
310
+ - Left as is: the full-suite timeouts the completionist recorded are release qualification, not code; Codex reruns the full suite on the final candidate in a quiet window.
311
+
312
+ **Upgrading from 6.99.33:** nothing to do. To keep the 6.99.33 behavior for one part, set `COS_STEER_MIDTURN=0` (steering), `COS_WEB_SESSION_FILES=0` or the Settings switch (session files and images), or `COS_CALENDAR_SNAPSHOT=0` (calendars). The table, divider and "May have been sent" changes have no switch.
313
+
314
+ **Known limits:** not yet accepted on a physical iPhone Duo (the table and divider checks are headless renders at Duo sizes). COS Control's own Sessions pane on the Mac does not show session files yet. The full limits of each part are in the 6.99.34 and 6.99.35 sections below.
315
+
316
+ **Tests:** the 6.99.34 and 6.99.35 suites below, plus calendar-source (an email-shaped attendee name is dropped, its mutation killed) and package-manifest (7.0.0 as the newest heading). The QA fixes add 14 tests in session-files-setting, session-files-setting-web, calendar-source, reader-files-web and today-attach-web; 12 failed on the unfixed source, and 22 of 22 mutants of the new guards were killed from a green baseline. The server version compare in every client was checked for a 7.x major before the bump (COS Control helper and app, the glasses app, the site's drift check): all compare by number.
317
+
318
+ ## 6.99.35 - Steer a busy session mid-turn, files your sessions send, Duo tables and resizable panes
319
+
320
+ 2026-11-07. On 6.99.34 (afc7cb2, installed privately). Private build for Miles's Mac; npm stays on 6.99.33. Server and Control Web only; COS Control stays on 0.5.378, and no glasses, phone or Control update is needed. Miles approved the default-on steering change.
321
+
322
+ **Default-on changes:**
323
+
324
+ *Steering a busy session (`COS_STEER_MIDTURN=0` restores 6.99.34):*
325
+ - **A message sent to a busy Claude session lands inside its running turn, at its next tool step (`COS_STEER_MIDTURN`, default on; `0` restores the turn-end wait).** Before, a message from Control Web, the phone or the glasses to a session that was working waited for the turn to end, and a session kept busy by background-task notifications almost never had a gap, so it could wait a long time. Now the park answer asks the drain to run at once, and the drain puts the words into the session through its peer inbox (the transport the live hop and 6.78.0 Session steer already use). Claude Code takes the message immediately (its `enqueue` row is the proof) and reads it at its next tool step. The row settles `delivered` with "Sent into the running turn." Same for Claude Code in the desktop app and the CLI in a terminal. Glasses messages get the same treatment.
326
+ - **A queued message COS cannot tell went in reads "May have been sent", not "Not sent".** In the Sessions reader's queue list, Today's sheet line and the Sessions row chip, a row with `deliveryState: unknown` (a steer it could not confirm, a delivery cut off by a restart) has its own label and its own line ("May not have been sent. Read it, then say it again."), never a "Not sent:" prefix. A message refused for tampering still reads "Not sent".
327
+ - **Control Web says which one applies.** Attachability gains `midTurn` (`{available, mode}` or `{available: false, reason}`) on a busy answer, and the park answer gains `landsAt` (`next_step` or `turn_end`), both only with the switch on. The composer reads "Working right now. Your message lands at its next step." and, after a send, "The session is busy. Yours lands at its next step." only when the server says so; otherwise the old turn-end lines.
328
+
329
+ *Files and images your sessions make (`COS_WEB_SESSION_FILES=0`, or the Settings switch, turns them off):*
330
+ - **Files a Claude session sent show in the Sessions reader and Today's session sheet.** The Claude app's SendUserFile tool shares files inline; Control Web showed nothing for them. Each share now appears where it happened in the conversation, as an assistant card with its caption.
331
+ - PNG, JPEG, WebP, HEIC and AVIF show as thumbnails that open full size, through the same image route and workspace rule as 6.99.28's output images.
332
+ - Every other file is a card with its name, type and size. A PDF opens in a new tab. Markdown, text, CSV, JSON, YAML, XML and SVG open as words in the viewer (an SVG is shown as its source, never as a picture from this origin). An HTML page shows in a sandboxed frame with scripts off and its own origin, or downloads. A GIF opens as a picture. Anything else (Word, audio, an EHPK) downloads. Every download is typed as a plain file, so no browser opens it as a page of COS.
333
+ - A file outside the session's workspace, behind a link that leaves it, or no longer on disk shows "Not available on this Mac" with no button. Only the file's base name ever reaches the browser, never a Mac path.
334
+ - A share's caption never shows a path: an image written into it reads "[Output image]". Past 10 files in one share, the rest are counted in words.
335
+ - **A switch for all of it: "Show files and images your sessions make"** in Control Web Settings, Privacy, with one sentence of help ("Lets this page show images and files your sessions save in their project folder. Files stay on your Mac."). **On by default**, covering these files and 6.99.28's images. Off: the output-images and output-files routes answer 403 `previews_off` before reading anything, and each message that had images or files shows one line, "Previews are off. Turn them on in Settings, Privacy.", with no file names or paths sent. Stored in `data/web-session-files-settings.json` through `GET`/`POST /api/session-files/settings`, read on every request. **`COS_WEB_SESSION_FILES=0`** (or false, off) on the Mac forces it off and wins over Settings; the row then says so and is disabled. Listed in `.env.example` and the managed-runtime contract.
336
+ - **New route** `GET /api/agent-sessions/:provider/:sessionId/output-files/:fileId/content`, behind the same /api gate. The id is opaque and is re-checked against the session's newest 40 turns on every read; the canonical path must still be inside the exact workspace, with no link in the last component and the same file before and after the read; 32 MB at most (413 above). Headers: PDF inline as application/pdf; text as text/plain; GIF as image/gif; HTML and everything else as an application/octet-stream attachment. Every response is nosniff and no-store, and everything but a PDF carries `Content-Security-Policy: sandbox; default-src 'none'`.
337
+
338
+ *Duo tables and the Sessions divider:*
339
+ - **Table words stay whole.** The panes set `overflow-wrap: anywhere`, which lets a table squeeze a column to one letter. Table cells now wrap at spaces only, so a column is never narrower than its longest word. Only a token of 25 or more characters (a URL, a hash) may break inside.
340
+ - **Column floors** (app.js `tableFit`, shared): a column whose text is longer than 10 characters keeps at least 10; a first column of short labels (up to 3 words and 28 characters) stays on one line; a longer first column gets 14 to 26 characters and wraps at words.
341
+ - **A table too wide for its box scrolls inside it**, with the first column held at the left edge. The page never scrolls sideways. Same on the folded Duo. The look is unchanged (the separated border model draws the same 1 px lines, so the held column keeps its own).
342
+ - The same applies to every markdown table in Control Web: the Sessions reader, Today's session sheet, Meetings notes and Ask, and Memories answers. Messages and Work chat do not draw tables (their markdown has none), so nothing changed there.
343
+ - **Drag the line between the Sessions list and the reader** (760 px and up, where both show). Finger, mouse or pen; focus it and use the arrow keys (Shift for bigger steps, Home, End). The list keeps 240 px and the reader 380 px, also after the screen turns. A double tap or double click goes back to the default width. On touch the target is 44 px. The width is remembered on this device; without storage it lasts the visit.
344
+ - **Full width / Show list** in the reader's header hides the list and brings it back, for when dragging is fiddly. Remembered the same way.
345
+
346
+ *Resizable panes across Control Web:*
347
+ - **Drag the line between the two columns of Memories, Messages, Meetings and Work too.** The Sessions divider is now one kit (`splitKit` in app.js) used by every two-column pane, each with its own remembered width (stored on this device; every read and write guarded, so without storage a redrawn pane keeps this visit's width). Memories: list and reader, 760 px and up, with Full width in the reader's header. Messages with the COS chat: list and chat, 860 px and up; the chat keeps 440 px so its composer stays usable. Meetings: notes or live transcript and the Ask panel, 860 px and up. Work: the card and its side panel, 900 px and up. Each divider sits in the gap that was already there, so default layouts are unchanged. Below a pane's breakpoint the divider is hidden and ignores input, so phones and the folded Duo do not change. Threads, Speakers, Settings and Today have no two columns side by side and get no divider.
348
+
349
+ ### Steer mid-turn
350
+
351
+ 2026-11-07. On 6.99.33 (02dd4a7). Server and Control Web. No client update is needed: the park answer is still 202 `queued: true`, so the glasses companion, the phone and COS Control read it exactly as before. Miles, 2026-11-07: "Yes I want to take that approach. Should land in about a min versus the whole [turn]. That way we can provide steer to our sessions." Findings, per provider: `MU-Chief-Staff/operations/personal/wk45_2026/steer_midturn_findings.md`.
352
+
353
+ **What keeps the old behaviour, and why:**
354
+ - Codex: `turn/steer` exists in the app-server protocol, but the running turn lives in ChatGPT.app's own app-server process and COS holds no handle on it (it spawns its own short-lived app-server for queue edits only). A busy Codex thread still takes the message into the app's own queue, which runs it after the turn.
355
+ - Cursor: the IDE composer's only door is its Stop hook; an Agent CLI chat has no input channel. Unchanged.
356
+ - Claude, when a COS child holds the thread (a Continue's resume child), the session is held on a permission prompt (hooks on), the thread is fenced, a cancel hold is on, or no peer inbox is open: queued for the turn end, as before.
357
+ - A message with files waits for the turn end (the peer inbox carries words only), and so does every message parked behind it.
358
+
359
+ **How it decides (and never sends twice):**
360
+ - FIFO per thread: the drain walks rows in order and stops steering at the first row that cannot go in, or at a row still being delivered.
361
+ - Only a row never attempted is steered (`attempts === 0`): a row with a spent front-door attempt or a restart requeue waits for the turn end.
362
+ - The intent row (`delivering`, `mode: steer`, `journal: 1`, the boot) and the attempt (`pending`) are written durably before the frame, in the 6.78.0 Session steer order. Unverified, a failed write or a throw settles `refused` / `steer_unverified` / `deliveryState: unknown` ("May not have been sent"), never resent. Nothing reached the session: back to waiting with the attempt given back, and the front door sends it at the turn end. A steer row a dead boot left is unknown at the next boot unless the journal says sent.
363
+ - Provenance at the steer is the front door's: footer-shaped words refused (`footer_line_refused`), the parked origin carried on a single-use carry spent in process, its seal checked (provider, thread, no files; `queued_turn_tampered`, "Not sent: its words, files or session changed."), a turn id held for another turn refused (`turn_id_in_use`), the footer written only by the server, the ledger record continued as `via: drain-steer` (or `drain-steer-unverified`). Words changed in the queue file after parking go in unvouched, with no footer, as the front door does.
364
+ - The park kicks the shared drain (one pass in flight, ever), so a park never starts a second concurrent pass.
365
+
366
+ **Canary (2026-11-07 20:43, Claude Code 2.1.319, disposable session, the real drain):** acceptance 50 ms after send, read in at the next tool step 4.41 s (attached to the result of the `sleep 8` call that was running), the session's reaction 6.85 s. A forged-footer row parked ahead of it was refused `footer_line_refused` and never reached the session. The delivered footer verifies with `verify_g2_turn.py`'s own unwrap, strip and hash.
367
+
368
+ **Known limits:**
369
+ - **Verifier (MU-Chief-Staff `verify_g2_turn.py`, outside this package):** read in mid-turn, Claude Code wraps the words as `<system-reminder id=...>` + "Another Claude session sent a message while you were working:". The verifier's peer head first accepted only the idle form; MU-Chief-Staff 0085536 (2026-11-07) widened `PEER_HEAD_RE` to accept that frame too, with near-miss frames still refused. A Mac running an older verifier fails closed on a G2 Tier 1 turn that arrives mid-turn.
370
+ - "Next tool step" means the session's next tool boundary: a long single tool call (a build, a long test) delays it until that call returns. A turn with no further tool call reads it after its answer.
371
+ - With hooks off, a session held on a permission prompt cannot be seen, so the message goes in and lands when the prompt is answered (the 6.78.0 limit).
372
+ - The glasses companion still shows its own parked-row line at park time; the row then reads "Sent into the running turn." on its next queue read.
373
+
374
+ **Tests:** steer-midturn (the switch and its docs; steered through the peer inbox and never through the front door; intent row and attempt before the frame; switch off and live hop off hold; only a busy gate is steered, not a COS child, a fence, a held prompt, no inbox or a cancel hold; Codex and Cursor untouched; FIFO in order, files ahead hold the row behind, a delivering row ahead holds; never twice: spent attempt, unverified, write failure, throw, not sent then once at turn end, journal refusal, dead boot reconcile; provenance: G2 footer and ledger, changed words unvouched, sealed files removed, forged footer line, reused turn id, park refusal; the park answer's `landsAt` and kick, switch off byte-identical), today-actions (the composer lines), pi-durable-flags-off (the mid-turn switch off with the other P3 switches).
375
+
376
+ ### Files a session sent
377
+
378
+ 2026-11-07. On the Duo tables work (806b4c3). Server (Control Web) plus one new read-only route.
379
+
380
+ Miles: "I can see the previews from claude but not from the COS control. This is one of the elements that need to also be patched and hot fixed."
381
+
382
+ **Not covered:**
383
+ - Codex and Cursor transcripts have no file-share record (Codex's ImageView is the agent looking at an image for itself, not sending it), so there is nothing to show there beyond the Markdown images 6.99.28 already covers.
384
+ - COS Control's Sessions pane on the Mac does not show these files yet (Swift; later, in the Swift slot).
385
+
386
+ **Tests:** agent-session-sent-files.test.ts (new): parsing, kinds, workspace and link rules, missing files and directories, URLs and home shorthand, a broad root, the per-row cwd, caption redaction, the cap, re-authorization, a link swapped in after listing, the size cap, the header policy and the route. reader-files-web.test.ts (new): the real reader-media.js and app.js DTO filter on fake elements (cards, "Not available on this Mac", junk ids, the PDF retype, the sandboxed HTML frame, octet-stream downloads, text as words, a 413). session-files-setting.test.ts and session-files-setting-web.test.ts (new): the switch and its file, the env override, the 403s before any read, the names stripped from the detail, the settings route, the Settings row and the off line.
387
+
388
+ ### Duo tables
389
+
390
+ 2026-11-07. On 6.99.33 (02dd4a7). Server only (Control Web); no route changed.
391
+
392
+ Miles on the unfolded Duo (IMG_0508, IMG_0509): tables in a session reply broke words one letter per line, and the first column ("Toda y", "Sess ion shee t") broke mid-word with room to spare. He asked whether the line between the Sessions list and the reader could be dragged.
393
+
394
+ **Tests:** duo-tables-web.test.ts (new, 24): tableFit, sxMd and the Memories renderer run from the real files; the divider runs from app.js on fake elements (drag, the lift's position, limits, double tap, a cancel, a second finger, right click, keys, storage that throws or holds junk, Full width); the stylesheet and page facts. reader-media-web.test.ts gives openSession its new sxFullButton dependency. 51 mutations from a green baseline, each landing exactly once, all killed. Headless Chrome renders before and after at 1430x1000, 1000x1430 and 382x678: words broken mid-word went from 3 to 54 per table to 0.
395
+
396
+ ### Resizable panes
397
+
398
+ **Tests:** split-panes-web.test.ts (new, 30): the kit from the real app.js on fake elements for every pane's options (per-pane keys, storage that throws, the narrow no-op, limits, reset, keys, mirrored side panels, Full width wiring), plus the panes' markup and stylesheet facts. The Sessions divider's 24 duo-tables tests pass unchanged on the shared kit.
399
+
400
+ ## 6.99.34 - Calendars from macOS Calendar (PH plan P1)
401
+
402
+ 2026-11-07. On 6.99.33 (02dd4a7, installed privately). Private build for Miles's Mac with COS Control 0.5.378, which writes the calendar snapshot. npm stays on 6.99.33.
403
+
404
+ **Default-on changes:** none for anyone who has not turned on Calendars in COS Control (Settings, Activity home, Calendars: off until Connect, nothing picked after the grant). With no snapshot file, every calendar read works exactly as in 6.99.33. Control Web Today asks `GET /api/calendar/today` once per paint and shows nothing new when it answers "not connected".
405
+
406
+ **What it does once Control writes a snapshot:**
407
+ - **One calendar source.** `lib/calendar-source.ts` reads `~/.cos-glasses/data/calendar/snapshot.json`. Snapshot 2 h old or less: used. Older: the starter-kit bridge if it reads, else the snapshot labeled "as of". Over 24 h, past its window, or an access-denied tombstone: no calendar ("not connected", never an empty day). `COS_CALENDAR_SNAPSHOT=0` ignores the file.
408
+ - **Selected calendars only**, the same meeting on two accounts once, and two holds on one calendar stay two.
409
+ - **Next up and prep skip** declined, all-day and multi-day events, holiday and birthday calendars, and shared-calendar events with no attendees (family flights).
410
+ - **Prep knows the meeting.** Prep pings and Drop it items carry the event's occurrence, so Prep me opens that occurrence's draft, and a moved meeting keeps its draft.
411
+ - **Control Web Today** lists up to two more of today's meetings under Next up, and says "Calendar as of h:mm" when the snapshot is aging.
412
+ - **`/api/models`** reports where the calendar comes from (`calendar`: source, age, access, counts). Counts only, never titles. Not on public `/api/health`.
413
+
414
+ **Known limits:** Meeting Ask gets no suggested domain for a snapshot event and asks for one. Desktop Today's later rows, a Meetings "Upcoming" strip and a Settings move are proposed, not built (operations/personal/wk45_2026/TEST_PLAN_calendar_p1_2026-11-07.md).
415
+
416
+ **Tests:** calendar-source (19: staleness, tombstone, selection, dedupe, prep filter, occurrence keys, counts-only status, no titles in logs), welcome-context-snapshot (3), today-calendar-web (4), the 6.89.0 /api/models golden (+ `calendar`). 12 guards mutation-checked. Control's real snapshot output parsed by `parseSnapshot`.
417
+
1
418
  ## 6.99.33 - Duo follow-ups: a hung Mac, and the Other field under the keyboard
2
419
 
3
420
  2026-11-07. On 6.99.30 (a90f024, installed privately 17:20). 6.99.32 (98f3cde) was the same work installed privately at 18:09 without the small-frames keyboard fix below; 6.99.33 replaces it. Server only (Control Web); no route changed, so it works with any COS Control that runs 6.99.29. The first public build with the 6.99.30 Duo layout work below; npm goes from 6.99.29 straight to 6.99.33.
package/README.md CHANGED
@@ -473,7 +473,9 @@ settled on evidence, never sent twice), `COS_PIN_BOOT_REAP` (default on: a threa
473
473
  run held frees 31 minutes after the restart, once its child is gone), `COS_QUEUE_RESEND`
474
474
  (default on: Send again on a refused or expired queued turn), `COS_MESSAGES_STEER` (default
475
475
  off: steer a running Claude Ask from the glasses), `COS_SESSION_STEER` (default off: steer a
476
- busy Claude desk session through its peer inbox) and `COS_QUERY_JOB_DELTA_JOURNAL` (default
476
+ busy Claude desk session through its peer inbox when a client asks), `COS_STEER_MIDTURN` (default
477
+ on: a message parked at a busy Claude desk session lands inside the running turn at its next tool
478
+ step, through the same peer inbox; `0` makes it wait for the turn to end) and `COS_QUERY_JOB_DELTA_JOURNAL` (default
477
479
  off until the release after 6.78.0: the compact Ask journal writer),
478
480
  `COS_PERMISSION_BROKER` (6.52.0: `0` answers every permission request with
479
481
  the Mac's own dialog, `questions` holds questions but not tool approvals; unset holds
@@ -92,7 +92,10 @@
92
92
  "COS_PIN_BOOT_REAP",
93
93
  "COS_QUEUE_RESEND",
94
94
  "COS_SESSION_STEER",
95
+ "COS_STEER_MIDTURN",
95
96
  "COS_KEY_LANES",
97
+ "COS_WEB_SESSION_FILES",
98
+ "COS_WEB_LIVE_EVENTS",
96
99
  "COS_DICTATION_AUTOCLEAN_MAX_PER_DAY",
97
100
  "COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_MESSAGE",
98
101
  "COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_MEETING",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.99.33",
3
+ "version": "7.0.7",
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": {