@gotcos/glasses-server 6.99.33 → 7.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
cap: null, // capabilities.sessionQuestions, once read
|
|
33
33
|
capAt: 0,
|
|
34
34
|
items: [], // pending items, oldest first
|
|
35
|
+
waiting: [], // X1 contract: the questions live sessions wait on (held, at_mac, answered)
|
|
36
|
+
webAnswer: null, // X1 final contract: what an answer from this page must carry right now (only with a hold switch on)
|
|
35
37
|
picks: {}, // id -> [{ labels: [], other: '' }] per question
|
|
36
38
|
armed: {}, // id -> 'send' | 'allow' | 'deny'
|
|
37
39
|
armedAt: {}, // id -> when it was armed (COSWeb.confirmReady)
|
|
@@ -47,7 +49,8 @@
|
|
|
47
49
|
stopped: false,
|
|
48
50
|
inflight: false, // a poll is on the wire
|
|
49
51
|
again: false, // something asked for a poll while one was out: one more as soon as it lands
|
|
50
|
-
lastPollAt: 0
|
|
52
|
+
lastPollAt: 0, // when the last poll went out (Date.now)
|
|
53
|
+
load: 'unread' // W13: 'unread' (no read yet), 'ok' (the last read was the server's answer) or 'failed' (it was not)
|
|
51
54
|
};
|
|
52
55
|
var DEFAULT_POLL_MS = 10000;
|
|
53
56
|
var POLL_TIMEOUT_MS = 15000;
|
|
@@ -58,7 +61,12 @@
|
|
|
58
61
|
// A nudge (a session that just went to waiting, Today coming on screen) never reads more often than this.
|
|
59
62
|
var NUDGE_GAP_MS = 1000;
|
|
60
63
|
// Miles's words for a question at the Mac (11/07), one place: Today's Needs you reads them from here too.
|
|
61
|
-
|
|
64
|
+
// W1 A5 (Miles 11/09, "there is no ability to answer the question"): said once, and honest that this page cannot answer it.
|
|
65
|
+
// Q1 M3 (X1 canary A1; lib/thread-turn-queue.ts:451): a message sent to a session held on its prompt is queued for the
|
|
66
|
+
// end of the turn. It does not answer the question, and nothing promises a way to answer it here.
|
|
67
|
+
var MAC_LINE = "Answer this at your Mac's prompt. A message sent from here does not answer it; it waits until the question is answered at the Mac.";
|
|
68
|
+
// A question that went straight to the Mac's prompt: no route carries its words (the hook keeps none), so say so.
|
|
69
|
+
var MAC_NO_WORDS = "COS cannot read this question's words from here. They show at the Mac's prompt.";
|
|
62
70
|
var ANSWERED_LINE = 'Answered. The session carries on.';
|
|
63
71
|
var ANSWERED_MS = 120000;
|
|
64
72
|
|
|
@@ -134,6 +142,19 @@
|
|
|
134
142
|
// QA 6.99.27 (N4): the read asked for while this one was out goes through nudge, so it waits out the 1 s gap too.
|
|
135
143
|
if (Q.again) { Q.again = false; nudge(); }
|
|
136
144
|
}
|
|
145
|
+
// W13 (QA week of 11/01-11/08): whether the last read was the server's answer. A failed read leaves no cards, and Today's
|
|
146
|
+
// sheet read "no card" as "at the Mac's prompt" (a Claude session) with no way to read again. Each change tells the
|
|
147
|
+
// mounted sheets (their onChange), never on a read that changed nothing.
|
|
148
|
+
function noteLoad(state) {
|
|
149
|
+
if (Q.load === state) return;
|
|
150
|
+
Q.load = state;
|
|
151
|
+
tellMounts();
|
|
152
|
+
}
|
|
153
|
+
function tellMounts() {
|
|
154
|
+
mounts.forEach(function (m) {
|
|
155
|
+
if (m.onChange) { try { m.onChange(itemsFor(m.sessionId).length, atMacFor(m.sessionId).length); } catch (_e) {} }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
137
158
|
function poll() {
|
|
138
159
|
clearTimeout(Q.timer);
|
|
139
160
|
if (Q.stopped || document.hidden || !signedIn()) return;
|
|
@@ -141,7 +162,7 @@
|
|
|
141
162
|
if (Q.inflight) { Q.again = true; return; }
|
|
142
163
|
Q.inflight = true; Q.again = false; Q.lastPollAt = Date.now();
|
|
143
164
|
capForPoll().then(function (cap) {
|
|
144
|
-
if (!usable(cap)) { Q.again = false; landed(); clearCards(); schedule(60000); return; }
|
|
165
|
+
if (!usable(cap)) { Q.again = false; landed(); clearCards(); schedule(60000); noteLoad(cap ? 'ok' : 'failed'); return; }
|
|
145
166
|
// 6.99.32 (QA 6.99.30 canary W1): a Mac that hangs (asleep, a stale Tailscale route) never answered and never
|
|
146
167
|
// refused, so this read stayed in flight forever: no more polls, and the Mac never marked down. It gives up after
|
|
147
168
|
// POLL_TIMEOUT_MS, says the Mac is not answering, and polls again. The limit covers the body too (QA 6.99.32
|
|
@@ -153,27 +174,32 @@
|
|
|
153
174
|
// answered (any status) or did not (below); otherwise a "down" mark stranded Submit, Allow and Deny for minutes.
|
|
154
175
|
if (W.noteServer) W.noteServer(true);
|
|
155
176
|
if (res.status === 401) { clearTimeout(limit); Q.again = false; landed(); Q.stopped = true; clearCards(); return; }
|
|
156
|
-
if (!res.ok) { clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed(); return; }
|
|
177
|
+
if (!res.ok) { clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed'); return; }
|
|
157
178
|
return res.json().then(function (d) {
|
|
158
179
|
clearTimeout(limit);
|
|
159
|
-
if (!d || d.enabled === false) { Q.again = false; landed(); clearCards(); schedule(60000); return; }
|
|
180
|
+
if (!d || d.enabled === false) { Q.again = false; landed(); clearCards(); schedule(60000); noteLoad('ok'); return; }
|
|
160
181
|
// Painted as soon as the read lands: take() draws the cards and repaints Today's Needs you and the pet.
|
|
161
|
-
take(
|
|
182
|
+
// The state first, so a redraw take() makes already reads 'ok'; a read that changed no card redraws nothing, so
|
|
183
|
+
// the sheets still hear that the failure is over.
|
|
184
|
+
var was = Q.load;
|
|
185
|
+
Q.load = 'ok';
|
|
186
|
+
take(Array.isArray(d.items) ? d.items : [], Array.isArray(d.settled) ? d.settled : [], Array.isArray(d.waiting) ? d.waiting : null, d.webAnswer);
|
|
187
|
+
if (was !== 'ok') tellMounts();
|
|
162
188
|
var every = Number(d.pollIntervalMs) > 0 ? Number(d.pollIntervalMs) : DEFAULT_POLL_MS;
|
|
163
189
|
schedule(nextWait(every));
|
|
164
190
|
landed();
|
|
165
191
|
}, function () {
|
|
166
192
|
// The body never finished (cut off, or the limit fired mid-body). The Mac did answer the headers, so it is not
|
|
167
193
|
// marked down; the poll is freed and goes again.
|
|
168
|
-
clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed();
|
|
194
|
+
clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed');
|
|
169
195
|
});
|
|
170
196
|
}, function () {
|
|
171
197
|
// No answer at all: refused, unreachable, or the limit fired before the headers (the only abort this read has).
|
|
172
198
|
clearTimeout(limit);
|
|
173
199
|
if (W.noteServer) W.noteServer(false);
|
|
174
|
-
schedule(DEFAULT_POLL_MS); landed();
|
|
200
|
+
schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed');
|
|
175
201
|
});
|
|
176
|
-
}).catch(function () { schedule(DEFAULT_POLL_MS); landed(); });
|
|
202
|
+
}).catch(function () { schedule(DEFAULT_POLL_MS); landed(); noteLoad('failed'); });
|
|
177
203
|
}
|
|
178
204
|
// Read now, unless a read went out under NUDGE_GAP_MS ago (then at the end of that gap) or one is on the wire (then
|
|
179
205
|
// once more when it lands). Today calls this when a session goes to waiting, so its question shows at once.
|
|
@@ -188,7 +214,28 @@
|
|
|
188
214
|
function stop() { clearTimeout(Q.timer); }
|
|
189
215
|
|
|
190
216
|
// ── state from a poll ─────────────────────────────────
|
|
191
|
-
|
|
217
|
+
// ── X1 contract v1 (COORD 2026-11-09 21:35): `waiting[]`, every AskUserQuestion a live Claude session waits on that the
|
|
218
|
+
// server saw asked: state held (a COS surface can answer it now), at_mac (only the Mac's own prompt can) or answered
|
|
219
|
+
// (kept 120 s). Words only in memory, as on the server. Keyed by `<sessionId>:<toolUseId>`, never by session alone.
|
|
220
|
+
var WAITING_STATES = { held: 1, at_mac: 1, answered: 1 };
|
|
221
|
+
function waitingEntry(w) {
|
|
222
|
+
if (!w || typeof w.key !== 'string' || !WAITING_STATES[w.state] || typeof w.sessionId !== 'string') return null;
|
|
223
|
+
var qs = (Array.isArray(w.questions) ? w.questions : []).slice(0, 4).map(function (q) {
|
|
224
|
+
return { question: String((q && q.question) || ''), header: String((q && q.header) || ''), multiSelect: !!(q && q.multiSelect),
|
|
225
|
+
options: (q && Array.isArray(q.options) ? q.options : []).slice(0, 8).map(function (o) { return { label: String((o && o.label) || ''), description: String((o && o.description) || '') }; }) };
|
|
226
|
+
}).filter(function (q) { return q.question; });
|
|
227
|
+
return { key: w.key, sessionId: w.sessionId.toLowerCase(), provider: 'claude', toolUseId: typeof w.toolUseId === 'string' ? w.toolUseId : null, state: w.state,
|
|
228
|
+
answerable: w.answerable === true && w.state === 'held', heldId: typeof w.heldId === 'string' && /^pq_[a-f0-9]{24}$/.test(w.heldId) ? w.heldId : null,
|
|
229
|
+
questions: qs, askedAt: Date.parse(w.askedAt) || 0, answeredAt: Date.parse(w.answeredAt) || 0 };
|
|
230
|
+
}
|
|
231
|
+
function take(items, settled, waiting, webAnswer) {
|
|
232
|
+
// An older server sends no `waiting`: keep the last list only for a moment's read, never invent one.
|
|
233
|
+
Q.waiting = Array.isArray(waiting) ? waiting.map(waitingEntry).filter(Boolean) : [];
|
|
234
|
+
// X1 final contract (`webAnswer`, sent with `waiting` while a hold switch is on): { touchRequired, touchMaxMs,
|
|
235
|
+
// confirmApprovals }. Absent: the page learns any rule from a refusal, as before.
|
|
236
|
+
Q.webAnswer = webAnswer && typeof webAnswer === 'object'
|
|
237
|
+
? { touchRequired: webAnswer.touchRequired === true, touchMaxMs: Number(webAnswer.touchMaxMs) > 0 ? Number(webAnswer.touchMaxMs) : 60000, confirmApprovals: webAnswer.confirmApprovals === true }
|
|
238
|
+
: null;
|
|
192
239
|
var pending = items.filter(function (i) { return i && i.answerable !== false && (i.kind === 'question' || i.kind === 'approval'); });
|
|
193
240
|
var atMac = (settled || []).filter(function (i) { return i && i.waitingAtMac === true && (i.kind === 'question' || i.kind === 'approval'); });
|
|
194
241
|
var before = Q.items.map(function (i) { return i.id; }).join('|') + '/' + Q.atMac.map(function (i) { return i.id; }).join('|');
|
|
@@ -230,9 +277,11 @@
|
|
|
230
277
|
return n > 0 && (q.multiSelect || n === 1);
|
|
231
278
|
});
|
|
232
279
|
}
|
|
280
|
+
// X1 contract: a question held for the whole wait (fullHold) is on the Mac's screen too. Both answers count; the first wins.
|
|
281
|
+
var FULL_HOLD_LINE = 'Answer here or at your Mac. The first answer counts.';
|
|
233
282
|
function noteHtml(item) {
|
|
234
283
|
var text = Q.notes[item.id] || '';
|
|
235
|
-
return '<p class="sq-note' + (text ? '' : ' hidden') + '" data-sq-note="' + esc(item.id) + '" role="status">' + esc(text) + '</p>';
|
|
284
|
+
return (item.fullHold === true ? '<p class="sq-fullhold">' + esc(FULL_HOLD_LINE) + '</p>' : '') + '<p class="sq-note' + (text ? '' : ' hidden') + '" data-sq-note="' + esc(item.id) + '" role="status">' + esc(text) + '</p>';
|
|
236
285
|
}
|
|
237
286
|
// The card reads like Claude Code's own question in the terminal (Miles 11/07): a strip naming each question, ☒ once
|
|
238
287
|
// it has an answer, then ✓ Submit; each question in bold, numbered options with their descriptions under them, Other
|
|
@@ -338,11 +387,22 @@
|
|
|
338
387
|
// A question or an approval the Mac's own prompt waits on: what it asks when this page saw it held, and where to answer.
|
|
339
388
|
// No buttons: nothing here can deliver an answer to that prompt.
|
|
340
389
|
function macCard(item) {
|
|
390
|
+
if (item.fromWaiting) {
|
|
391
|
+
var body0 = (item.questions || []).map(function (q) {
|
|
392
|
+
return '<p class="sq-q-text sq-mac-q">' + esc(q.question) + '</p>' + (q.options.length ? '<ul class="sq-mac-opts">' + q.options.map(function (o) {
|
|
393
|
+
return '<li>' + esc(o.label) + (o.description ? '<span class="sq-mac-desc"> ' + esc(o.description) + '</span>' : '') + '</li>';
|
|
394
|
+
}).join('') + '</ul>' : '');
|
|
395
|
+
}).join('') || '<p class="sq-mac-nowords">' + esc(MAC_NO_WORDS) + '</p>';
|
|
396
|
+
return '<article class="sq-card sq-mac" data-sq-mac="' + esc(item.id) + '" data-kind="question">' +
|
|
397
|
+
'<header class="sq-head"><h2 class="sq-title">' + esc(engine(item)) + ' has a question</h2></header>' + body0 +
|
|
398
|
+
'<p class="sq-mac-line">' + esc(MAC_LINE) + '</p></article>';
|
|
399
|
+
}
|
|
341
400
|
var words = Q.seen[item.id] || { questions: [], summary: '' };
|
|
342
401
|
var title = item.kind === 'approval' ? engine(item) + ' wants to use ' + (item.tool || 'a tool') : engine(item) + ' has a question';
|
|
343
402
|
var body = item.kind === 'approval'
|
|
344
403
|
? (words.summary ? '<pre class="sq-command">' + esc(words.summary) + '</pre>' : '')
|
|
345
404
|
: words.questions.map(function (q) { return '<p class="sq-q-text sq-mac-q">' + esc(q) + '</p>'; }).join('');
|
|
405
|
+
if (!body) body = '<p class="sq-mac-nowords">' + esc(MAC_NO_WORDS) + '</p>';
|
|
346
406
|
return '<article class="sq-card sq-mac" data-sq-mac="' + esc(item.id) + '" data-kind="' + esc(item.kind) + '">' +
|
|
347
407
|
'<header class="sq-head"><h2 class="sq-title">' + esc(title) + '</h2></header>' + body +
|
|
348
408
|
'<p class="sq-mac-line">' + esc(MAC_LINE) + '</p></article>';
|
|
@@ -351,7 +411,24 @@
|
|
|
351
411
|
// Every item names its session (the broker validates it), so no session asked for matches none: the top of every
|
|
352
412
|
// pane never draws a Mac card.
|
|
353
413
|
var want = String(sessionId || '').toLowerCase();
|
|
354
|
-
|
|
414
|
+
// X1 contract: a question at the Mac's own prompt that this server saw asked has its words in `waiting`. One card per
|
|
415
|
+
// question, matched by tool_use_id: a held item that went back to the Mac and the waiting entry are the same question.
|
|
416
|
+
var waits = Q.waiting.filter(function (w) { return w.sessionId === want && w.state === 'at_mac'; });
|
|
417
|
+
var cards = waits.map(function (w) {
|
|
418
|
+
return { id: 'w:' + w.key, kind: 'question', sessionId: w.sessionId, provider: 'claude', toolUseId: w.toolUseId, fromWaiting: true, questions: w.questions };
|
|
419
|
+
});
|
|
420
|
+
var ids = {}; waits.forEach(function (w) { if (w.toolUseId) ids[w.toolUseId] = 1; });
|
|
421
|
+
var settled = Q.atMac.filter(function (i) {
|
|
422
|
+
if (String(i.sessionId || '').toLowerCase() !== want) return false;
|
|
423
|
+
if (i.toolUseId) return !ids[i.toolUseId];
|
|
424
|
+
// An older server gives a settled item no tool_use_id: show it only when no waiting entry speaks for the session.
|
|
425
|
+
return !waits.length;
|
|
426
|
+
});
|
|
427
|
+
return settled.concat(cards);
|
|
428
|
+
}
|
|
429
|
+
function waitingFor(sessionId) {
|
|
430
|
+
var want = String(sessionId || '').toLowerCase();
|
|
431
|
+
return Q.waiting.filter(function (w) { return w.sessionId === want; });
|
|
355
432
|
}
|
|
356
433
|
// True when this session's newest event here is an answer (from this page or any other screen) of the last two
|
|
357
434
|
// minutes, with nothing held for it now and nothing at the Mac since.
|
|
@@ -361,6 +438,12 @@
|
|
|
361
438
|
var at = 0, macAt = 0;
|
|
362
439
|
Object.keys(Q.answered).forEach(function (id) { var a = Q.answered[id]; if (a.sessionId === want && Date.now() - a.at <= ANSWERED_MS) at = Math.max(at, a.at); });
|
|
363
440
|
atMacFor(want).forEach(function (i) { macAt = Math.max(macAt, Date.parse(i.settledAt) || Date.parse(i.createdAt) || 0); });
|
|
441
|
+
// X1 contract: answered anywhere (the Mac's prompt or a COS surface) in the last 120 s, newer than any question still open.
|
|
442
|
+
Q.waiting.forEach(function (w) {
|
|
443
|
+
if (w.sessionId !== want) return;
|
|
444
|
+
if (w.state === 'answered' && Date.now() - w.answeredAt <= ANSWERED_MS) at = Math.max(at, w.answeredAt);
|
|
445
|
+
else if (w.state !== 'answered') macAt = Math.max(macAt, w.askedAt || Date.now());
|
|
446
|
+
});
|
|
364
447
|
return at > 0 && at >= macAt;
|
|
365
448
|
}
|
|
366
449
|
function wordsOf(id) { var w = Q.seen[id]; return w ? { questions: w.questions.slice(), summary: w.summary } : null; }
|
|
@@ -439,10 +522,32 @@
|
|
|
439
522
|
|
|
440
523
|
// ── answering ─────────────────────────────────────────
|
|
441
524
|
var REFUSED = {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
525
|
+
// Q1 (D4 card review M2, same server): already_answered is another COS device (a lens, the phone, Control or a browser);
|
|
526
|
+
// an answer at the Mac comes back as handed_to_desk with reason retracted (RETRACTED_LINE).
|
|
527
|
+
already_answered: 'Already answered on another device.', expired: 'This question timed out here. Answer it at the Mac.',
|
|
528
|
+
handed_to_desk: 'Left for the Mac. Answer it there.', hook_gone: 'This question is no longer waiting.',
|
|
529
|
+
not_found: 'This question is gone.', invalid_answer: 'That answer was not accepted. Check each question has a choice.',
|
|
530
|
+
// X1 contract, web door with a full-hold flag on: none of these settle the card; the person taps again.
|
|
531
|
+
web_touch_required: 'Not sent: tap anywhere on this page, then answer again.',
|
|
532
|
+
// X1 final contract (Q1 O1): held, but not on your devices yet (the Mac has not been idle long enough). Not settled.
|
|
533
|
+
not_shown: 'Not sent: this question is not on your devices yet. Answer it at the Mac, or try again in a minute.',
|
|
534
|
+
confirm_required: 'Not sent: tap again to confirm.', confirm_invalid: 'Not sent: that confirmation ran out. Tap again to confirm.'
|
|
445
535
|
};
|
|
536
|
+
var RETRACTED_LINE = 'Already answered at the Mac, or the session moved on.';
|
|
537
|
+
// The page's api() reads a body's `reason` before its `error` (other panes rely on that), so a refusal such as
|
|
538
|
+
// {error: 'handed_to_desk', reason: 'retracted'} or {error: 'invalid_answer', reason: 'client_answer_id'} arrives with the
|
|
539
|
+
// reason as its code and the error word as its message. Put the server's error word back as the code, the reason beside it.
|
|
540
|
+
function refusal(err) {
|
|
541
|
+
if (!err || err.status == null) return err;
|
|
542
|
+
var word = typeof err.message === 'string' ? err.message : '';
|
|
543
|
+
if (!REFUSED[err.code] && REFUSED[word]) { err.reason = err.code; err.code = word; }
|
|
544
|
+
return err;
|
|
545
|
+
}
|
|
546
|
+
function refusedLine(err) {
|
|
547
|
+
if (err && err.code === 'handed_to_desk' && err.reason === 'retracted') return RETRACTED_LINE;
|
|
548
|
+
return err ? REFUSED[err.code] : '';
|
|
549
|
+
}
|
|
550
|
+
var KEEPS_CARD = { invalid_answer: 1, web_touch_required: 1, confirm_required: 1, confirm_invalid: 1, not_shown: 1 };
|
|
446
551
|
// QA round 1: an answer that never reached the server (no HTTP status: offline, a dropped link, a timeout) says so in
|
|
447
552
|
// plain words, with the button to tap; the retry goes under the same clientAnswerId, so the server replays it.
|
|
448
553
|
function unreachable(item, what) {
|
|
@@ -458,10 +563,37 @@
|
|
|
458
563
|
changed();
|
|
459
564
|
if (W.today && W.today.refresh) { try { W.today.refresh(); } catch (_e) {} }
|
|
460
565
|
}
|
|
566
|
+
// X1 contract: the web door sends how long ago this page was last touched (the tap that sent it counts). An older server
|
|
567
|
+
// or a flags-off server ignores the field.
|
|
568
|
+
function withTouch(body) {
|
|
569
|
+
var age = presenceAge();
|
|
570
|
+
if (age !== null) body.presenceAgeMs = age;
|
|
571
|
+
return body;
|
|
572
|
+
}
|
|
573
|
+
function confirmThen(id, body, doneLine) {
|
|
574
|
+
Q.sending[id] = 1; render();
|
|
575
|
+
var c = withTouch({ decision: body.decision });
|
|
576
|
+
api('/api/session-questions/' + encodeURIComponent(id) + '/confirm', { method: 'POST', body: c, timeoutMs: 20000 }).then(function (d) {
|
|
577
|
+
if (!d || typeof d.confirmId !== 'string' || !/^pc_[a-f0-9]{24}$/.test(d.confirmId)) throw Object.assign(new Error('confirm'), { status: 403, code: 'confirm_invalid' });
|
|
578
|
+
body.confirmId = d.confirmId;
|
|
579
|
+
post(id, body, doneLine);
|
|
580
|
+
}, function (err) {
|
|
581
|
+
delete Q.sending[id];
|
|
582
|
+
err = refusal(err);
|
|
583
|
+
var settles = err && REFUSED[err.code] && !KEEPS_CARD[err.code];
|
|
584
|
+
if (settles) { Q.items = Q.items.filter(function (i) { return i.id !== id; }); render(); changed(); W.toast(refusedLine(err)); schedule(500); return; }
|
|
585
|
+
Q.notes[id] = refusedLine(err) || (err && err.status == null ? unreachable(itemOf(id), body.decision) : 'Not sent: ' + ((err && err.message) || 'the Mac did not answer') + ' Tap again to retry.');
|
|
586
|
+
render();
|
|
587
|
+
});
|
|
588
|
+
}
|
|
461
589
|
function post(id, body, doneLine) {
|
|
590
|
+
// X1 final contract: when the server says approvals need a confirm, get it first rather than learn it from a refused
|
|
591
|
+
// answer. It still follows the person's second tap; a 403 confirm_required below stays the fallback.
|
|
592
|
+
if (body.decision && !body.confirmId && Q.webAnswer && Q.webAnswer.confirmApprovals) return confirmThen(id, body, doneLine);
|
|
462
593
|
Q.sending[id] = 1; Q.armed[id] = ''; Q.notes[id] = '';
|
|
463
594
|
if (!Q.answerIds[id]) Q.answerIds[id] = answerId();
|
|
464
595
|
body.clientAnswerId = Q.answerIds[id];
|
|
596
|
+
withTouch(body);
|
|
465
597
|
render();
|
|
466
598
|
var asked = itemOf(id);
|
|
467
599
|
api('/api/session-questions/' + encodeURIComponent(id) + '/answer', { method: 'POST', body: body, timeoutMs: 20000 }).then(function () {
|
|
@@ -473,16 +605,22 @@
|
|
|
473
605
|
schedule(500);
|
|
474
606
|
}, function (err) {
|
|
475
607
|
delete Q.sending[id];
|
|
476
|
-
|
|
608
|
+
err = refusal(err);
|
|
609
|
+
// X1 contract: an approval the server wants confirmed gets its confirm (bound to this item and decision, 60 s, single
|
|
610
|
+
// use) and goes once more. The person already tapped twice; nothing is sent without a second tap.
|
|
611
|
+
if (err && err.status === 403 && err.code === 'confirm_required' && body.decision && !body.confirmId) {
|
|
612
|
+
return confirmThen(id, body, doneLine);
|
|
613
|
+
}
|
|
614
|
+
var settled = err && REFUSED[err.code] && !KEEPS_CARD[err.code];
|
|
477
615
|
if (settled) {
|
|
478
616
|
if (err.code === 'already_answered') noteAnswered(asked, id);
|
|
479
617
|
Q.items = Q.items.filter(function (i) { return i.id !== id; });
|
|
480
618
|
render();
|
|
481
619
|
if (err.code === 'already_answered') answeredTold(); else changed();
|
|
482
|
-
W.toast(
|
|
620
|
+
W.toast(refusedLine(err)); schedule(500); return;
|
|
483
621
|
}
|
|
484
622
|
// A timeout or a lost answer: the same clientAnswerId goes again on the next tap and the server replays it.
|
|
485
|
-
Q.notes[id] =
|
|
623
|
+
Q.notes[id] = refusedLine(err) || (err && err.status == null ? unreachable(asked, body.decision || 'send')
|
|
486
624
|
: 'Not sent: ' + ((err && err.message) || 'the Mac did not answer') + ' Tap again to retry.');
|
|
487
625
|
render();
|
|
488
626
|
});
|
|
@@ -547,11 +685,13 @@
|
|
|
547
685
|
Q.armed[id] = '';
|
|
548
686
|
if (!Q.answerIds[id]) Q.answerIds[id] = answerId();
|
|
549
687
|
Q.sending[id] = 1; render();
|
|
550
|
-
api('/api/session-questions/' + encodeURIComponent(id) + '/answer', { method: 'POST', body: { clientAnswerId: Q.answerIds[id], handBack: true }, timeoutMs: 20000 }).then(function () {
|
|
688
|
+
api('/api/session-questions/' + encodeURIComponent(id) + '/answer', { method: 'POST', body: withTouch({ clientAnswerId: Q.answerIds[id], handBack: true }), timeoutMs: 20000 }).then(function () {
|
|
551
689
|
delete Q.sending[id]; Q.items = Q.items.filter(function (i) { return i.id !== id; }); render(); W.toast('Left for the Mac.'); schedule(500);
|
|
552
690
|
}, function (err) {
|
|
553
691
|
delete Q.sending[id];
|
|
554
|
-
|
|
692
|
+
err = refusal(err);
|
|
693
|
+
if (err && REFUSED[err.code] && !KEEPS_CARD[err.code]) { Q.items = Q.items.filter(function (i) { return i.id !== id; }); render(); changed(); W.toast(refusedLine(err)); return; }
|
|
694
|
+
if (err && KEEPS_CARD[err.code]) { Q.notes[id] = refusedLine(err); render(); return; }
|
|
555
695
|
Q.notes[id] = err && err.status == null ? unreachable(item, 'leave') : 'Not sent: ' + ((err && err.message) || 'the Mac did not answer'); render();
|
|
556
696
|
});
|
|
557
697
|
return;
|
|
@@ -707,5 +847,6 @@
|
|
|
707
847
|
if (W.onRoute) W.onRoute(function (r) { if (r && r.pane === 'today') nudge(); });
|
|
708
848
|
|
|
709
849
|
W.sessionQuestions = { _state: Q, _take: take, _render: render, _presenceAge: presenceAge, _pollUrl: pollUrl, _key: onKey, _advanceMs: ADVANCE_MS, _todayPollMs: TODAY_POLL_MS, _fitKeyboard: fitKeyboard, poll: poll, nudge: nudge, mount: mount,
|
|
710
|
-
itemsFor: itemsFor, atMacFor: atMacFor, answeredFor: answeredFor, wordsOf: wordsOf, macLine: MAC_LINE, answeredLine: ANSWERED_LINE
|
|
850
|
+
itemsFor: itemsFor, atMacFor: atMacFor, waitingFor: waitingFor, answeredFor: answeredFor, wordsOf: wordsOf, fullHoldLine: FULL_HOLD_LINE, macLine: MAC_LINE, macNoWords: MAC_NO_WORDS, answeredLine: ANSWERED_LINE,
|
|
851
|
+
loadState: function () { return Q.load; }, retry: function () { nudge(); } };
|
|
711
852
|
})();
|