@gotcos/glasses-server 6.99.25 → 6.99.33

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 (53) hide show
  1. package/CHANGELOG.md +224 -0
  2. package/package.json +1 -1
  3. package/server/index.ts +46 -4
  4. package/server/lib/agent-session-output-images.ts +280 -0
  5. package/server/lib/agent-session-store.ts +3 -1
  6. package/server/lib/agent-session-turns.ts +18 -7
  7. package/server/lib/client-liveness.ts +38 -3
  8. package/server/lib/codex-subagent-activity.ts +36 -0
  9. package/server/lib/g2-turn-provenance.ts +679 -0
  10. package/server/lib/media-store.ts +41 -2
  11. package/server/lib/permission-broker.ts +94 -17
  12. package/server/lib/queue-turn-evidence.ts +19 -2
  13. package/server/lib/session-halt.ts +15 -1
  14. package/server/lib/session-stream-events.ts +4 -0
  15. package/server/lib/session-subagents.ts +155 -0
  16. package/server/lib/session-turn-attachments.ts +245 -0
  17. package/server/lib/thread-turn-queue-deliver.ts +17 -2
  18. package/server/lib/thread-turn-queue.ts +10 -0
  19. package/server/lib/today-brief.ts +731 -0
  20. package/server/lib/turn-file-store.ts +202 -0
  21. package/server/lib/turn-footer.ts +81 -0
  22. package/server/routes/agent-session-bindings.ts +164 -16
  23. package/server/routes/agent-sessions.ts +77 -8
  24. package/server/routes/client-instance.ts +12 -1
  25. package/server/routes/cursor-stop-followup.ts +30 -1
  26. package/server/routes/g2-turns.ts +46 -0
  27. package/server/routes/health.ts +9 -0
  28. package/server/routes/media.ts +32 -8
  29. package/server/routes/permission-broker.ts +47 -4
  30. package/server/routes/thread-turn-queue.ts +148 -8
  31. package/server/routes/today-brief.ts +148 -0
  32. package/server/web-control/app.css +42 -9
  33. package/server/web-control/app.js +975 -114
  34. package/server/web-control/control.css +12 -6
  35. package/server/web-control/index.html +6 -1
  36. package/server/web-control/panes/cos-chat.css +27 -3
  37. package/server/web-control/panes/cos-chat.js +240 -46
  38. package/server/web-control/panes/meetings.css +5 -2
  39. package/server/web-control/panes/meetings.js +21 -10
  40. package/server/web-control/panes/memories.css +1 -1
  41. package/server/web-control/panes/memories.js +3 -8
  42. package/server/web-control/panes/questions.css +56 -2
  43. package/server/web-control/panes/questions.js +454 -56
  44. package/server/web-control/panes/reader-media.js +154 -0
  45. package/server/web-control/panes/sessions.css +20 -7
  46. package/server/web-control/panes/settings.js +34 -2
  47. package/server/web-control/panes/speakers.css +3 -5
  48. package/server/web-control/panes/speakers.js +7 -4
  49. package/server/web-control/panes/threads.js +3 -2
  50. package/server/web-control/panes/today-actions.css +81 -3
  51. package/server/web-control/panes/today-actions.js +527 -62
  52. package/server/web-control/panes/work.css +1 -1
  53. package/server/web-control/shell.js +3 -0
@@ -102,7 +102,7 @@
102
102
  // "2026-11-03 · 9:05 PM · P · 60 minutes · Mac System Audio"
103
103
  function subtitle(m) {
104
104
  var parts = [];
105
- if (m.date) parts.push(m.date);
105
+ if (m.date) parts.push(W.plainDay ? W.plainDay(m.date) : m.date); // "Today" or "Nov 7", never 2026-11-07 (6.99.30)
106
106
  if (m.time) parts.push(fmtTime(m.time));
107
107
  var d = domainLabel(m); if (d) parts.push(d);
108
108
  if (m.duration) parts.push(m.duration); else if (num(m.durationMinutes) > 0) parts.push(num(m.durationMinutes) + ' min');
@@ -2352,7 +2352,13 @@
2352
2352
  function prepCapsNow() { var cc = W.cosChat; return cc && typeof cc.prepCaps === 'function' ? cc.prepCaps() : null; }
2353
2353
  function livePrep(sid) { if (!S.lprep || S.lprep.sid !== sid) S.lprep = { sid: sid, asked: false, state: 'idle', drafts: [], busy: '', done: null, note: '', changed: '' }; return S.lprep; }
2354
2354
  function notePrepInstance(b) { var cc = W.cosChat; if (b && cc && typeof cc.notePrepInstance === 'function') cc.notePrepInstance(b.serverInstanceId); }
2355
- function prepBoundTo(d, sid) { return ((d && d.bindings) || []).some(function (b) { return b && b.sessionId === sid && !b.removedAt; }); }
2355
+ function prepBoundTo(d, sid) { return ((d && d.bindings) || []).some(function (b) { return b && b.sessionId === sid && !b.removedAt && b.state !== 'applying'; }); }
2356
+ // An interrupted bind owns frozen rows at its original revision. A later editable draft is not its retry.
2357
+ function prepPendingBind(d, sid) {
2358
+ return ((d && d.bindings) || []).filter(function (b) {
2359
+ return b && b.sessionId === sid && !b.removedAt && b.state === 'applying' && Number.isSafeInteger(b.revision) && b.revision >= 0;
2360
+ })[0] || null;
2361
+ }
2356
2362
  function prepReadyFiles(d) { return ((d && d.files) || []).filter(function (x) { return x && x.status === 'ready'; }); }
2357
2363
  function prepHasNotes(d) { return !!String((d && d.steer) || '').trim(); }
2358
2364
  // Whether this live view may offer a prep at all: the area is reserved exactly when this is true.
@@ -2376,7 +2382,7 @@
2376
2382
  if (S.lprep !== p) return;
2377
2383
  notePrepInstance(r.body);
2378
2384
  p.drafts = r.status === 200 && Array.isArray(r.body.drafts) ? r.body.drafts.filter(function (d) {
2379
- return d && typeof d.id === 'string' && !prepBoundTo(d, f.sid) && (prepHasNotes(d) || prepReadyFiles(d).length > 0);
2385
+ return d && typeof d.id === 'string' && !prepBoundTo(d, f.sid) && (prepPendingBind(d, f.sid) || prepHasNotes(d) || prepReadyFiles(d).length > 0);
2380
2386
  }).slice(0, 5) : [];
2381
2387
  p.state = r.status === 200 ? 'ready' : 'failed';
2382
2388
  paintLivePrep();
@@ -2388,7 +2394,7 @@
2388
2394
  return prepHasNotes(d) ? (n ? 'its notes and ' + files : 'its notes') : 'its ' + files;
2389
2395
  }
2390
2396
  function prepAddBtn(p, d) {
2391
- return '<button type="button" class="cx-btn quiet" data-mt="prep-bind" data-id="' + esc(d.id) + '"' + (p.busy ? ' disabled' : '') + '>' + (p.busy === d.id ? 'Adding…' : 'Add to this meeting') + '</button>';
2397
+ return '<button type="button" class="cx-btn quiet" data-mt="prep-bind" data-id="' + esc(d.id) + '"' + (p.busy ? ' disabled' : '') + '>' + (p.busy === d.id ? 'Adding…' : prepPendingBind(d, p.sid) ? 'Retry add prep' : 'Add to this meeting') + '</button>';
2392
2398
  }
2393
2399
  function prepQuiet(text, warn) { return '<div class="mt-prepline" role="status"><span class="mt-prepline-text' + (warn ? ' mt-amber' : '') + '">' + esc(text) + '</span></div>'; }
2394
2400
  function livePrepHtml() {
@@ -2402,14 +2408,14 @@
2402
2408
  else if (!p.drafts.length) html = prepQuiet('No prep for this meeting. Make one from a Prep me card in COS chat.');
2403
2409
  else if (p.drafts.length === 1) {
2404
2410
  var d = p.drafts[0], title = cleanTitle(d.title) || 'this meeting';
2405
- var text = p.note || 'Prep for ' + title + (p.changed === d.id ? ' changed since it was offered: add ' : ' is ready: add ') + prepWhat(d) + ' to this meeting';
2411
+ var text = p.note || (prepPendingBind(d, p.sid) ? 'Finish adding the saved prep for ' + title + '. Later edits are not included.' : 'Prep for ' + title + (p.changed === d.id ? ' changed since it was offered: add ' : ' is ready: add ') + prepWhat(d) + ' to this meeting');
2406
2412
  html = '<div class="mt-prepline"><span class="mt-prepline-text' + (p.note ? ' mt-amber' : '') + '" title="' + esc(text) + '">' + esc(text) + '</span>' + prepAddBtn(p, d) + '</div>';
2407
2413
  } else {
2408
2414
  // Several: the person picks. Each row says what it adds; the list scrolls inside the same box.
2409
2415
  html = '<div class="mt-prepline list"><ul class="mt-preplist" aria-label="' + esc(p.drafts.length + ' preps are ready for this meeting') + '">' +
2410
2416
  (p.note ? '<li class="mt-prepnote"><span class="mt-prepline-text mt-amber">' + esc(p.note) + '</span></li>' : '') +
2411
2417
  p.drafts.map(function (d) {
2412
- var label = (cleanTitle(d.title) || 'Prep') + (p.changed === d.id ? ' (changed)' : '') + ': ' + prepWhat(d).replace(/^its /, '');
2418
+ var label = (cleanTitle(d.title) || 'Prep') + (prepPendingBind(d, p.sid) ? ': finish adding the saved prep. Later edits are not included.' : (p.changed === d.id ? ' (changed)' : '') + ': ' + prepWhat(d).replace(/^its /, ''));
2413
2419
  return '<li><span class="mt-prepline-text" title="' + esc(label) + '">' + esc(label) + '</span>' + prepAddBtn(p, d) + '</li>';
2414
2420
  }).join('') + '</ul></div>';
2415
2421
  }
@@ -2432,16 +2438,21 @@
2432
2438
  // v1.1: the bind names the revision the offer described ({revision, sessionId, serverInstanceId, surface}), so a newer
2433
2439
  // version nobody has seen is never attached. The server id is the newest one read: capabilities.prep or the most
2434
2440
  // recent Prep answer (W.cosChat.prepInstance), else Meeting Ask's, which is the same id.
2435
- function bindOnce(sid, d) {
2441
+ function bindOnce(sid, d, retry) {
2436
2442
  var cc = W.cosChat, known = cc && typeof cc.prepInstance === 'function' ? cc.prepInstance() : '';
2437
2443
  return (known ? Promise.resolve(known) : loadConfig().then(function (conf) { return conf && conf.serverInstanceId; })).then(function (inst) {
2438
2444
  if (!inst) throw new Error(S.conf.notice || 'Meeting assistance is not available on this server.');
2439
2445
  // Amendment 19: the header is what authorizes the write; the body carries the same id.
2440
- return answer('/api/prep/drafts/' + encodeURIComponent(d.id) + '/bind', { method: 'POST', headers: { 'X-COS-Expected-Server-Instance': inst }, body: { revision: d.revision, sessionId: sid, serverInstanceId: inst, surface: 'web' } });
2446
+ return answer('/api/prep/drafts/' + encodeURIComponent(d.id) + '/bind', { method: 'POST', headers: { 'X-COS-Expected-Server-Instance': inst }, body: { revision: retry ? retry.revision : d.revision, sessionId: sid, serverInstanceId: inst, surface: 'web' } });
2441
2447
  }).then(function (a) {
2442
2448
  var b = a.body || {}, code = typeof b.error === 'string' ? b.error : (b.error && b.error.code) || b.reason || '';
2443
2449
  if (a.status >= 200 && a.status < 300 || a.status === 409) notePrepInstance(b);
2444
- if (a.status >= 200 && a.status < 300) return b;
2450
+ if (a.status >= 200 && a.status < 300) {
2451
+ if (retry && !((b.draft && b.draft.bindings) || []).some(function (bound) {
2452
+ return bound && bound.sessionId === sid && bound.revision === retry.revision && bound.state === 'applied' && !bound.removedAt;
2453
+ })) throw new Error('The server has not confirmed adding this prep. Retry add prep.');
2454
+ return b;
2455
+ }
2445
2456
  // A different server than the one that offered it: the prep is read again (amendment 19) and the line says so.
2446
2457
  // Adding it takes another tap, against the server that answers now.
2447
2458
  if (a.status === 409 && code === 'server_instance_mismatch') {
@@ -2469,7 +2480,7 @@
2469
2480
  if (!f || !p || p.sid !== f.sid || p.busy || p.done || f.endedAt) return;
2470
2481
  var d = p.drafts.filter(function (x) { return x.id === id; })[0]; if (!d) return;
2471
2482
  p.busy = id; p.note = ''; paintLivePrep();
2472
- bindOnce(f.sid, d).then(function (b) {
2483
+ bindOnce(f.sid, d, prepPendingBind(d, f.sid)).then(function (b) {
2473
2484
  p.busy = ''; p.done = (b && b.draft) || d;
2474
2485
  var a = A(f.sid);
2475
2486
  // The meeting's files as the bind answered them, when it answered Meeting Ask files; and read again either way.
@@ -128,7 +128,7 @@
128
128
  .mm-try { margin: 8px 0 2px; }
129
129
  .mm-tries { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; }
130
130
  .mm-tries .mm-link { font-size: 13.5px; overflow-wrap: anywhere; }
131
- .mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; overflow-y: auto; }
131
+ .mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; max-height: 60dvh; overflow-y: auto; }
132
132
  .mm-answer h4 { margin: 10px 0 4px; font: 600 14.5px/1.3 var(--body); }
133
133
  .mm-answer p { margin: 0 0 8px; }
134
134
  .mm-answer ul { margin: 0 0 8px; padding-left: 1.2em; }
@@ -109,19 +109,14 @@
109
109
  // Copy stays in this browser: the same copy the rest of Control Web uses (plain-http fallback included).
110
110
  if (op === 'copy') return W.copyText(String(a.text || '')).then(function () { return {}; });
111
111
  var limit = READ_LIMIT[op] != null ? READ_LIMIT[op] : (isRead(op, a) ? 45000 : 0);
112
- return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(null, plainNetError).then(function (r) {
112
+ return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(function (r) {
113
113
  if (r && r.ok) return r.details || {};
114
114
  var raw = (r && r.message) || 'Your Mac did not answer that.';
115
115
  var err = new Error(PLAIN[raw] || raw); err.code = raw; throw err;
116
116
  });
117
117
  }
118
- // No HTTP status means the request never reached COS: the browser's own words ("Failed to fetch", "Load failed") say
119
- // nothing to a person, so say what happened and what to do. A timeout already says it in plain words (W.api).
120
- var UNREACHABLE = 'Could not reach COS on your Mac. Check that the Mac is awake and on Tailscale, then try again.';
121
- function plainNetError(err) {
122
- if (err && !err.status && err.code !== 'timeout') { var e = new Error(UNREACHABLE); e.code = 'unreachable'; throw e; }
123
- throw err;
124
- }
118
+ // A request that never reached COS is said in plain words by W.api itself (app.js, 6.99.30: code 'unreachable'),
119
+ // as a timeout is: nothing to translate here.
125
120
  function codeOf(e) { return String((e && (e.code || e.message)) || ''); }
126
121
 
127
122
  // ── state ────────────────────────────────────────────────
@@ -1,4 +1,4 @@
1
- /* COS Control Web: session questions and approvals (panes/questions.js), server 6.99.12. */
1
+ /* COS Control Web: session questions and approvals (panes/questions.js), from server 6.99.12. */
2
2
  .sq-host { display: grid; gap: 12px; margin: 0 0 16px; }
3
3
  .sq-host[hidden] { display: none; }
4
4
  .sq-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); border-radius: 16px; padding: 14px 16px 12px; min-width: 0; }
@@ -6,7 +6,7 @@
6
6
  .sq-title { margin: 0; font: 600 16px/1.3 var(--body); color: var(--ink); overflow-wrap: anywhere; }
7
7
  .sq-secs { font: 500 12.5px/1.3 var(--body); color: var(--hot); white-space: nowrap; }
8
8
  .sq-q { border: 0; margin: 12px 0 0; padding: 0; min-width: 0; }
9
- .sq-q-text { padding: 0; font: 500 15px/1.4 var(--body); color: var(--ink); overflow-wrap: anywhere; }
9
+ .sq-q-text { padding: 0; font: 700 15.5px/1.4 var(--body); color: var(--ink); overflow-wrap: anywhere; }
10
10
  .sq-q-tag { font-size: 12.5px; font-weight: 500; color: var(--tertiary); }
11
11
  .sq-fine { margin: 2px 0 0; font-size: 12.5px; color: var(--tertiary); }
12
12
  .sq-options { display: grid; gap: 2px; margin-top: 8px; }
@@ -27,6 +27,60 @@
27
27
  .sq-leave { min-height: 44px; font-size: 14px; font-weight: 500; color: var(--ink); }
28
28
  .sq-note { margin: 8px 0 0; font-size: 13px; color: var(--hot); }
29
29
  .sq-note.hidden { display: none; }
30
+ /* 6.99.26: read like Claude Code's question in the terminal (Miles 11/07): the strip, numbered options, Other as the
31
+ last number, the answers to review, Submit. */
32
+ .sq-steps { display: flex; flex-wrap: wrap; gap: 0 14px; margin: 8px 0 0; padding-bottom: 6px; border-bottom: 1px solid var(--row-border); }
33
+ .sq-step { min-height: 36px; padding: 0; font: 500 13.5px/1.2 var(--mono); color: var(--sub); white-space: nowrap; }
34
+ .sq-step:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
35
+ .sq-num { flex: none; min-width: 1.6em; font: 500 14px/1.45 var(--mono); color: var(--sub); }
36
+ .sq-option-other { align-items: center; cursor: text; }
37
+ .sq-option-other .sq-other { flex: 1; width: auto; min-width: 0; margin-top: 0; }
38
+ .sq-review { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px; background: var(--qopen); }
39
+ .sq-review-head { margin: 0 0 4px; font: 600 13px/1.3 var(--body); color: var(--sub); }
40
+ .sq-review-list { margin: 0; padding-left: 1.4em; display: grid; gap: 4px; font-size: 14px; line-height: 1.4; }
41
+ .sq-review-q { font-weight: 600; color: var(--ink); margin-right: 8px; }
42
+ .sq-review-a { color: var(--ink); overflow-wrap: anywhere; }
43
+ .sq-review-a.empty { color: var(--tertiary); }
44
+ /* 6.99.27 (Miles's phone test): what each question needs, and a footer that keeps the progress and the answer button
45
+ on screen while the card is, at every Duo size. Questions clear the top bar and the footer when scrolled to. */
46
+ .sq-need { margin: 2px 0 0; font-size: 13px; line-height: 1.4; }
47
+ .sq-need[data-state="todo"] { color: var(--hot); font-weight: 600; }
48
+ .sq-need[data-state="done"] { color: var(--tertiary); }
49
+ .sq-q { scroll-margin-top: calc(var(--topbar-h, 48px) + var(--safe-top, 0px) + 12px); scroll-margin-bottom: 150px; }
50
+ .ta-q .sq-q { scroll-margin-top: 8px; }
51
+ .sq-review { scroll-margin-bottom: 150px; }
52
+ .sq-foot { position: sticky; bottom: var(--safe-bottom, 0px); z-index: 2; margin: 12px -16px -12px; padding: 10px 16px 12px; background: var(--panel);
53
+ border-top: 1px solid var(--row-border); border-radius: 0 0 16px 16px; }
54
+ .sq-foot .sq-acts { margin-top: 0; }
55
+ /* QA 6.99.27: on the page, the footer rides above whatever covers the bottom (questions.js sets --sq-kb from app.js
56
+ viewportFit: a keyboard, its strip, a browser's bottom bar). In Today's sheet the layer is itself the visible part of
57
+ the page (today-actions.js applyFit), so the footer sits at the bottom of the sheet with no gap. */
58
+ .sq-host .sq-foot { bottom: max(var(--safe-bottom, 0px), var(--sq-kb, 0px)); }
59
+ .ta-q .sq-foot { bottom: 0; }
60
+ /* 6.99.30 (canary): with a keyboard up on the page (questions.js sets sq-kb-up) the footer scrolls with the card, so
61
+ the options it would cover stay readable. */
62
+ .sq-host.sq-kb-up .sq-foot { position: static; }
63
+ /* The Mac not answering: Submit, Allow and Deny wait, and say why (questions.js offline). */
64
+ .sq-offline { margin: 8px 0 0; font-size: 13px; color: var(--sub); }
65
+ /* 6.99.30 (canary): folded, a waiting question is one line over the pane (it filled the first screen of every pane);
66
+ a tap opens the cards. From 600 px the cards show as before and the line does not. */
67
+ .sq-banner { display: none; }
68
+ @media (max-width: 599px) {
69
+ .sq-host .sq-banner { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 10px 14px; border-radius: 14px;
70
+ background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); color: var(--ink); font: 600 15px/1.3 var(--body); text-align: left; }
71
+ .sq-host .sq-banner-text { flex: 1; min-width: 0; }
72
+ .sq-host .sq-banner-act { flex: none; color: var(--accent); }
73
+ .sq-host .sq-cards:not(.open) { display: none; }
74
+ }
75
+ .sq-cards { display: grid; gap: 12px; }
76
+ .sq-progress { margin: 0 0 8px; font: 500 13.5px/1.35 var(--body); color: var(--sub); }
77
+ .sq-progress[data-alert] { color: var(--hot); font-weight: 600; }
78
+ .sq-keys { display: none; margin-top: 10px; }
79
+ @media (hover: hover) and (pointer: fine) { .sq-keys { display: block; } }
80
+ /* 6.99.26: a question the Mac's own prompt waits on. Read-only, so it does not take the waiting colour. */
81
+ .sq-card.sq-mac { border-color: var(--panel-border); }
82
+ .sq-mac-q { margin: 10px 0 0; }
83
+ .sq-mac-line { margin: 10px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--sub); }
30
84
  @media (max-width: 480px) {
31
85
  .sq-acts .cx-btn { flex: 1; justify-content: center; }
32
86
  }