@gotcos/glasses-server 6.71.4 → 6.71.9
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 +4 -0
- package/CHANGELOG.md +29 -0
- package/README.md +39 -14
- package/bin/hooks/cos-session-hook +15 -3
- package/docs/test-plan-6.71.5.md +47 -0
- package/docs/test-plan-6.71.7.md +56 -0
- package/docs/test-plan-6.71.8.md +123 -0
- package/docs/test-plan-6.71.9.md +61 -0
- package/package.json +7 -2
- package/server/index.ts +5 -1
- package/server/lib/claude-hooks-installer.ts +1 -0
- package/server/lib/live-cos-ask.ts +95 -28
- package/server/lib/meeting-ask-evidence.ts +59 -0
- package/server/lib/meeting-ask-followup.ts +27 -0
- package/server/lib/meeting-ask-grounding.ts +57 -0
- package/server/lib/meeting-ask-runtime.ts +102 -21
- package/server/lib/meeting-ask-store.ts +3 -0
- package/server/lib/meeting-ask-web.ts +22 -4
- package/server/lib/meeting-image-ocr.ts +57 -0
- package/server/lib/meeting-public-topic.ts +61 -0
- package/server/lib/meeting-web-page.ts +80 -0
- package/server/lib/native/meeting-image-ocr.js +23 -0
- package/server/lib/permission-broker.ts +76 -19
- package/server/routes/meeting-ask.ts +2 -2
- package/server/routes/permission-broker.ts +6 -2
- package/shared/meeting-ask.ts +7 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Apple Vision parity with COS Control's WorkCardFiles.recognizeText.
|
|
2
|
+
// Runs under macOS's built-in JXA; no developer tools or API key required.
|
|
3
|
+
ObjC.import('Foundation');
|
|
4
|
+
ObjC.import('Vision');
|
|
5
|
+
function run(args) {
|
|
6
|
+
if (args.length !== 1) throw new Error('invalid_input');
|
|
7
|
+
const request = $.VNRecognizeTextRequest.alloc.init;
|
|
8
|
+
request.recognitionLevel = 0; // VNRequestTextRecognitionLevelAccurate
|
|
9
|
+
request.usesLanguageCorrection = true;
|
|
10
|
+
const handler = $.VNImageRequestHandler.alloc.initWithURLOptions($.NSURL.fileURLWithPath(args[0]), $({}));
|
|
11
|
+
const error = Ref();
|
|
12
|
+
if (!handler.performRequestsError($([request]), error)) throw new Error('recognition_failed');
|
|
13
|
+
const results = request.results;
|
|
14
|
+
const lines = [];
|
|
15
|
+
for (let i = 0; i < results.count; i++) {
|
|
16
|
+
const candidates = results.objectAtIndex(i).topCandidates(1);
|
|
17
|
+
if (candidates.count) {
|
|
18
|
+
const box = results.objectAtIndex(i).boundingBox;
|
|
19
|
+
lines.push({text: ObjC.unwrap(candidates.objectAtIndex(0).string), confidence: Number(candidates.objectAtIndex(0).confidence), x: box.origin.x, y: box.origin.y + box.size.height / 2, height: box.size.height});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify({lines});
|
|
23
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// 6.71.7 stamped interactive question rule: hold from ask-time with a connected
|
|
2
|
+
// answerer/recent presence; show only once the desk reaches its idle threshold,
|
|
3
|
+
// then latch visibility until a winner, cancellation, expiry or shutdown. The
|
|
4
|
+
// native desk dialog remains available (Desktop D1 canary passed). A stamp is
|
|
5
|
+
// admitted only for AskUserQuestion outside background/child sessions. Unstamped
|
|
6
|
+
// questions and approvals follow the legacy gates documented below.
|
|
7
|
+
// Reinstall hooks once; hook-ask-hold=0 restores unstamped behavior.
|
|
8
|
+
//
|
|
1
9
|
// The permission broker (6.52.0): a Claude session's question card (AskUserQuestion) or
|
|
2
10
|
// tool approval, answered from the COS glasses or the phone while Miles is away from the
|
|
3
11
|
// Mac, and handed straight back to the Mac's own dialog the moment he returns to it.
|
|
@@ -307,6 +315,8 @@ export interface PermissionRequestFacts {
|
|
|
307
315
|
* writes one) or the payload. Cursor never reaches here (`cursor` refusal above).
|
|
308
316
|
*/
|
|
309
317
|
provider?: 'claude' | 'codex'
|
|
318
|
+
askHold?: boolean
|
|
319
|
+
toolUseId?: string | null
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
export type EnvelopeVerdict =
|
|
@@ -334,7 +344,7 @@ function readPermissionRequestEnvelope(body: unknown): EnvelopeRead {
|
|
|
334
344
|
const hookWaitS = typeof wait === 'number' && Number.isInteger(wait) && wait > 0 && wait <= HOOK_WAIT_STAMP_MAX_S ? wait : null
|
|
335
345
|
return {
|
|
336
346
|
ok: true,
|
|
337
|
-
facts: { sessionId: p.session_id.toLowerCase(), toolName: p.tool_name, toolInput: p.tool_input, hookStartedAtMs: ts, hookWaitS, provider: inferHookProvider(body.provider, p) === 'codex' ? 'codex' : 'claude' },
|
|
347
|
+
facts: { sessionId: p.session_id.toLowerCase(), toolName: p.tool_name, toolInput: p.tool_input, hookStartedAtMs: ts, hookWaitS, askHold: body.askHold === 1 && !p.agent_id, toolUseId: typeof p.tool_use_id === 'string' ? p.tool_use_id : null, provider: inferHookProvider(body.provider, p) === 'codex' ? 'codex' : 'claude' },
|
|
338
348
|
}
|
|
339
349
|
}
|
|
340
350
|
|
|
@@ -737,6 +747,8 @@ export interface PermissionBrokerDeps {
|
|
|
737
747
|
lastLegacyPollAt?(): number | null
|
|
738
748
|
/** 6.60.0: the newest counting poll WITH `hold=1` (`lastHoldQuestionsPollAt`). Absent: none. */
|
|
739
749
|
lastHoldPollAt?(): number | null
|
|
750
|
+
/** Known print/background/COS-spawned requests keep the legacy admission rules. */
|
|
751
|
+
isBackgroundRequest?(body: unknown): boolean
|
|
740
752
|
readDeskIdleSeconds(): Promise<number | null>
|
|
741
753
|
deskIdleSeconds(): number
|
|
742
754
|
timeoutMs(): number
|
|
@@ -821,6 +833,8 @@ export interface ParkRequest {
|
|
|
821
833
|
* still does). False with the away hold off, or for a request built without it.
|
|
822
834
|
*/
|
|
823
835
|
awayHold?: boolean
|
|
836
|
+
askHold?: boolean
|
|
837
|
+
lensVisible?: boolean
|
|
824
838
|
hold?: HoldFacts
|
|
825
839
|
}
|
|
826
840
|
|
|
@@ -852,6 +866,10 @@ interface BrokerItem {
|
|
|
852
866
|
timer: ReturnType<typeof setTimeout> | null
|
|
853
867
|
/** 6.60.0: held by the away hold (`ParkRequest.awayHold`). */
|
|
854
868
|
awayHold: boolean
|
|
869
|
+
askHold: boolean
|
|
870
|
+
lensVisible: boolean
|
|
871
|
+
toolUseId: string | null
|
|
872
|
+
questionFingerprint: string
|
|
855
873
|
/**
|
|
856
874
|
* 6.60.0: settled with `{}` written to the hook (handed to the desk, expired, no client,
|
|
857
875
|
* drained), so the Mac's own dialog is showing, and the session has not moved past the
|
|
@@ -975,13 +993,25 @@ const TURN_BOUNDARIES: ReadonlySet<string> = new Set(['Stop', 'StopFailure', 'Se
|
|
|
975
993
|
* means the Mac is no longer waiting on it. A question's tool is AskUserQuestion; an
|
|
976
994
|
* approval matches by fingerprint, or by tool name for a denial.
|
|
977
995
|
*/
|
|
978
|
-
function sessionMovedPast(item: Pick<BrokerItem, 'kind' | 'fingerprint' | 'toolName'>, env: HookEnvelope, toolName: string, fingerprint: string, strict = false): boolean {
|
|
979
|
-
if (TURN_BOUNDARIES.has(env.event) || (env.event === 'SessionStart' && env.payload.source !== 'compact')) return true
|
|
996
|
+
function sessionMovedPast(item: Pick<BrokerItem, 'kind' | 'fingerprint' | 'toolName' | 'askHold' | 'toolUseId' | 'questionFingerprint'>, env: HookEnvelope, toolName: string, fingerprint: string, strict = false): boolean {
|
|
997
|
+
if ((item.askHold && env.event === 'Interrupt') || TURN_BOUNDARIES.has(env.event) || (env.event === 'SessionStart' && env.payload.source !== 'compact')) return true
|
|
980
998
|
if (env.event === 'PostToolUse' || env.event === 'PostToolUseFailure' || env.event === 'PermissionDenied') {
|
|
981
999
|
// `strict` (QA round 1, N3), for "the Mac is no longer waiting": an approval only by its
|
|
982
1000
|
// own fingerprint, never by a denial of the same tool name, which may be another dialog
|
|
983
1001
|
// of the same session still on screen.
|
|
984
|
-
if (item.kind === 'question')
|
|
1002
|
+
if (item.kind === 'question') {
|
|
1003
|
+
if (toolName !== ASK_USER_QUESTION_TOOL) return false
|
|
1004
|
+
if (!item.askHold) return true // old hook compatibility
|
|
1005
|
+
if (item.toolUseId) return env.payload.tool_use_id === item.toolUseId
|
|
1006
|
+
// Desktop can add answers to the original tool input on completion. Match the
|
|
1007
|
+
// question payload itself so that a desk answer still closes exactly its card.
|
|
1008
|
+
const completed = env.payload.tool_input
|
|
1009
|
+
if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
|
|
1010
|
+
const questions = (completed as Record<string, unknown>).questions
|
|
1011
|
+
if (Array.isArray(questions)) return toolFingerprint(ASK_USER_QUESTION_TOOL, { questions }) === item.questionFingerprint
|
|
1012
|
+
}
|
|
1013
|
+
return fingerprint === item.fingerprint
|
|
1014
|
+
}
|
|
985
1015
|
if (strict) return fingerprint === item.fingerprint
|
|
986
1016
|
return fingerprint === item.fingerprint || (env.event === 'PermissionDenied' && toolName === item.toolName)
|
|
987
1017
|
}
|
|
@@ -1157,20 +1187,27 @@ export class PermissionBroker {
|
|
|
1157
1187
|
if (this.deps.mode() !== 'all') return this.noteFastPath('approvals_off')
|
|
1158
1188
|
if (UNBROKERED_TOOLS.has(facts.toolName)) return this.noteFastPath('unsupported_tool')
|
|
1159
1189
|
}
|
|
1190
|
+
let background = false
|
|
1191
|
+
try { background = this.deps.isBackgroundRequest?.(body) === true } catch { background = true }
|
|
1192
|
+
const askHold = isQuestion && facts.askHold === true && !background
|
|
1160
1193
|
const now = this.deps.now()
|
|
1161
1194
|
const presence = this.presence(now)
|
|
1162
|
-
|
|
1195
|
+
let holdPollAt: number | null = null
|
|
1196
|
+
try { holdPollAt = this.deps.lastHoldPollAt?.() ?? null } catch { /* legacy only */ }
|
|
1197
|
+
const connectedHoldClient = askHold && this.polledWithin(holdPollAt, now)
|
|
1198
|
+
const clientLive = this.clientLive(now) || presence.holdLive || connectedHoldClient
|
|
1163
1199
|
// 6.60.0: the away hold applies only when it is on, the installed hook waits longer than
|
|
1164
1200
|
// the 6.59.0 broker could hold, and a `hold=1` client reported wearer presence within the
|
|
1165
1201
|
// window (Miles, 19:54; QA round 1).
|
|
1166
1202
|
const switchOn = this.awayHoldMs() > 0
|
|
1167
1203
|
const ceilingAllowsAway = this.holdMs(facts.hookWaitS, false).ceilingAllowsAway
|
|
1168
|
-
const away = switchOn && ceilingAllowsAway && presence.recent
|
|
1204
|
+
const away = switchOn && ceilingAllowsAway && (askHold || presence.recent)
|
|
1169
1205
|
const notAwayReason: NotAwayReason | null = away ? null
|
|
1170
1206
|
: !switchOn ? 'hold_off'
|
|
1171
1207
|
: !ceilingAllowsAway ? 'ceiling_at_base'
|
|
1172
1208
|
: presence.ageMs === null ? 'no_hold_client'
|
|
1173
1209
|
: 'stale_presence'
|
|
1210
|
+
if (askHold && !clientLive && !presence.recent) return this.noteFastPath('no_client', { presence, clientLive })
|
|
1174
1211
|
const hold = this.holdMs(facts.hookWaitS, away)
|
|
1175
1212
|
// With it, a request is held whether or not a client is polling right now, so a lens or
|
|
1176
1213
|
// phone that comes back can still answer it. Without it, no client is `{}`, as in 6.59.0.
|
|
@@ -1183,8 +1220,8 @@ export class PermissionBroker {
|
|
|
1183
1220
|
if (this.pendingCount() >= MAX_PENDING) return this.noteFastPath('capacity')
|
|
1184
1221
|
let idle: number | null = null
|
|
1185
1222
|
try { idle = await this.deps.readDeskIdleSeconds() } catch { idle = null }
|
|
1186
|
-
if (idle === null) return this.noteFastPath('desk_unknown')
|
|
1187
|
-
if (idle < this.deskIdleThreshold()) return this.noteFastPath('desk_active', { notAway: 'desk_active', presence, clientLive })
|
|
1223
|
+
if (!askHold && idle === null) return this.noteFastPath('desk_unknown')
|
|
1224
|
+
if (!askHold && idle! < this.deskIdleThreshold()) return this.noteFastPath('desk_active', { notAway: 'desk_active', presence, clientLive })
|
|
1188
1225
|
const mode = this.deps.mode()
|
|
1189
1226
|
if (mode === 'off') return this.noteFastPath('broker_off')
|
|
1190
1227
|
if (!this.deps.admissionsOpen()) return this.noteFastPath('draining')
|
|
@@ -1210,13 +1247,13 @@ export class PermissionBroker {
|
|
|
1210
1247
|
awayHoldS: Math.round(hold.awayHoldMs / 1000),
|
|
1211
1248
|
ceilingS: hold.ceilingS,
|
|
1212
1249
|
effectiveDeadlineS: Math.round(hold.holdMs / 1000),
|
|
1213
|
-
deskIdleMs: idle * 1000,
|
|
1250
|
+
deskIdleMs: idle === null ? null : idle * 1000,
|
|
1214
1251
|
clientLive,
|
|
1215
1252
|
presenceAgeMs: presence.ageMs === null ? null : Math.round(presence.ageMs),
|
|
1216
1253
|
recentWindowS: Math.round(presence.windowMs / 1000),
|
|
1217
1254
|
notAwayReason,
|
|
1218
1255
|
}
|
|
1219
|
-
return { ok: true, request: { facts: { ...facts, fingerprint }, kind, questions, approval, deadlineAt, awayHold, hold: holdFacts } }
|
|
1256
|
+
return { ok: true, request: { facts: { ...facts, fingerprint }, kind, questions, approval, deadlineAt, awayHold, askHold, lensVisible: !askHold || (idle !== null && idle >= this.deskIdleThreshold()), hold: holdFacts } }
|
|
1220
1257
|
}
|
|
1221
1258
|
|
|
1222
1259
|
/** Hold the hook's response until exactly one resolution. Returns the item id. */
|
|
@@ -1246,6 +1283,10 @@ export class PermissionBroker {
|
|
|
1246
1283
|
channel,
|
|
1247
1284
|
timer: null,
|
|
1248
1285
|
awayHold: request.awayHold === true,
|
|
1286
|
+
askHold: request.askHold === true,
|
|
1287
|
+
lensVisible: request.lensVisible !== false,
|
|
1288
|
+
toolUseId: facts.toolUseId ?? null,
|
|
1289
|
+
questionFingerprint: toolFingerprint(ASK_USER_QUESTION_TOOL, { questions: facts.toolInput.questions }),
|
|
1249
1290
|
waitingAtMac: false,
|
|
1250
1291
|
}
|
|
1251
1292
|
this.items.set(id, item)
|
|
@@ -1293,6 +1334,8 @@ export class PermissionBroker {
|
|
|
1293
1334
|
return true
|
|
1294
1335
|
}
|
|
1295
1336
|
|
|
1337
|
+
questionsPollIntervalMs(): number { return this.pendingCount() > 0 ? 3_000 : QUESTIONS_POLL_INTERVAL_MS }
|
|
1338
|
+
|
|
1296
1339
|
/** The pending items, oldest first, for `GET /api/session-questions`. */
|
|
1297
1340
|
list(): SessionQuestionView[] {
|
|
1298
1341
|
const now = this.deps.now()
|
|
@@ -1301,6 +1344,7 @@ export class PermissionBroker {
|
|
|
1301
1344
|
if (item.state !== 'pending') continue
|
|
1302
1345
|
// A deadline the timer has not caught yet is still a dead question.
|
|
1303
1346
|
if (now >= item.deadlineAt) { this.settle(item, 'expired'); continue }
|
|
1347
|
+
if (!item.lensVisible) continue
|
|
1304
1348
|
views.push({
|
|
1305
1349
|
id: item.id,
|
|
1306
1350
|
sessionId: item.sessionId,
|
|
@@ -1359,10 +1403,10 @@ export class PermissionBroker {
|
|
|
1359
1403
|
if (wanted.length < 8) return 0
|
|
1360
1404
|
let cleared = 0
|
|
1361
1405
|
for (const item of this.items.values()) {
|
|
1362
|
-
if (!item.waitingAtMac) continue
|
|
1363
1406
|
if (item.sessionId !== wanted && !(wanted.length < 36 && item.sessionId.startsWith(wanted))) continue
|
|
1364
|
-
item.
|
|
1365
|
-
|
|
1407
|
+
if (item.askHold && item.state === 'pending') {
|
|
1408
|
+
if (this.settle(item, 'retracted')) cleared++
|
|
1409
|
+
} else if (item.waitingAtMac) { item.waitingAtMac = false; cleared++ }
|
|
1366
1410
|
}
|
|
1367
1411
|
if (cleared > 0) this.log(`session row ended: ${cleared} no longer waiting at the Mac session=${wanted.slice(0, 8)}`)
|
|
1368
1412
|
return cleared
|
|
@@ -1440,6 +1484,7 @@ export class PermissionBroker {
|
|
|
1440
1484
|
item.timer = null
|
|
1441
1485
|
this.counters.answered++
|
|
1442
1486
|
try { this.deps.signals?.decided(item.sessionId, item.id, item.fingerprint) } catch { /* rows are advisory */ }
|
|
1487
|
+
if (item.kind === 'question') this.log(`glasses_won ${item.id}`)
|
|
1443
1488
|
this.log(`answered ${item.id} kind=${item.kind} pending=${this.pendingCount()}`)
|
|
1444
1489
|
this.prune()
|
|
1445
1490
|
return { status: 200, body: { ok: true, id: item.id, kind: item.kind, ...chosen } }
|
|
@@ -1524,7 +1569,7 @@ export class PermissionBroker {
|
|
|
1524
1569
|
* dialog was answered), or its turn ended. An event older than the request is history.
|
|
1525
1570
|
*/
|
|
1526
1571
|
observeHookEvent(env: HookEnvelope, child: boolean): void {
|
|
1527
|
-
if (child) return
|
|
1572
|
+
if (child || env.payload.agent_id) return
|
|
1528
1573
|
const p = env.payload
|
|
1529
1574
|
const toolName = typeof p.tool_name === 'string' ? p.tool_name : ''
|
|
1530
1575
|
const fingerprint = typeof p.tool_fingerprint === 'string' ? p.tool_fingerprint : toolFingerprint(toolName, p.tool_input ?? null)
|
|
@@ -1546,7 +1591,10 @@ export class PermissionBroker {
|
|
|
1546
1591
|
continue
|
|
1547
1592
|
}
|
|
1548
1593
|
if (item.state !== 'pending' || env.ts < item.requestedAt) continue
|
|
1549
|
-
if (sessionMovedPast(item, env, toolName, fingerprint))
|
|
1594
|
+
if (sessionMovedPast(item, env, toolName, fingerprint)) {
|
|
1595
|
+
if (env.event === 'PostToolUse' && item.kind === 'question') this.log(`desk_won ${item.id}`)
|
|
1596
|
+
this.settle(item, 'retracted')
|
|
1597
|
+
}
|
|
1550
1598
|
}
|
|
1551
1599
|
}
|
|
1552
1600
|
|
|
@@ -1578,9 +1626,10 @@ export class PermissionBroker {
|
|
|
1578
1626
|
// the wearer's latest presence is still within the window (QA round 1, Skeptic W2: a
|
|
1579
1627
|
// single glance must not hold a question 10 more minutes). The desk below still hands
|
|
1580
1628
|
// it back.
|
|
1581
|
-
this.settleAll('no_client', item => !item.awayHold || !presence.recent)
|
|
1629
|
+
this.settleAll('no_client', item => !item.askHold && (!item.awayHold || !presence.recent))
|
|
1582
1630
|
if (this.pendingCount() === 0) return
|
|
1583
1631
|
}
|
|
1632
|
+
if (![...this.items.values()].some(item => item.state === 'pending' && (!item.askHold || !item.lensVisible))) return
|
|
1584
1633
|
if (this.pollInFlight) return
|
|
1585
1634
|
this.pollInFlight = true
|
|
1586
1635
|
let idle: number | null = null
|
|
@@ -1588,12 +1637,20 @@ export class PermissionBroker {
|
|
|
1588
1637
|
if (idle === null) {
|
|
1589
1638
|
this.counters.deskUnreadable++
|
|
1590
1639
|
this.deskUnreadableStreak++
|
|
1591
|
-
if (this.deskUnreadableStreak >= 2) this.settleAll('handed_to_desk')
|
|
1640
|
+
if (this.deskUnreadableStreak >= 2) this.settleAll('handed_to_desk', item => !item.askHold)
|
|
1592
1641
|
return
|
|
1593
1642
|
}
|
|
1594
1643
|
this.deskUnreadableStreak = 0
|
|
1595
|
-
if (idle >= this.deskIdleThreshold())
|
|
1596
|
-
|
|
1644
|
+
if (idle >= this.deskIdleThreshold()) {
|
|
1645
|
+
for (const item of this.items.values()) {
|
|
1646
|
+
if (item.state === 'pending' && item.askHold && !item.lensVisible && now < item.deadlineAt) {
|
|
1647
|
+
item.lensVisible = true
|
|
1648
|
+
this.log(`lens_visible ${item.id}`)
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return
|
|
1652
|
+
}
|
|
1653
|
+
this.settleAll('handed_to_desk', item => !item.askHold)
|
|
1597
1654
|
}
|
|
1598
1655
|
|
|
1599
1656
|
private settleAll(resolution: 'handed_to_desk' | 'drained' | 'no_client', only: (item: BrokerItem) => boolean = () => true): void {
|
|
@@ -99,8 +99,8 @@ export function createMeetingAskRouter(runtime: MeetingAskRuntime = meetingAskRu
|
|
|
99
99
|
catch (e) {
|
|
100
100
|
failure(res, e);
|
|
101
101
|
} });
|
|
102
|
-
router.post('/meeting-ask/files/media', (req, res) => { try {
|
|
103
|
-
res.json({ files: runtime.
|
|
102
|
+
router.post('/meeting-ask/files/media', async (req, res) => { try {
|
|
103
|
+
res.json({ files: await runtime.attachMediaReadable(String(req.body?.sessionId ?? ''), req.body?.serverInstanceId, req.body?.media) });
|
|
104
104
|
}
|
|
105
105
|
catch (e) {
|
|
106
106
|
failure(res, e);
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// 6.71.7: new-hook stamped interactive questions also arrive while the desk is
|
|
2
|
+
// active. The broker keeps their lens card hidden until the desk is idle and
|
|
3
|
+
// then latches it. Native desk answers/cancels settle the same request. Legacy
|
|
4
|
+
// hooks and approvals keep the gates below. GET polling is 3 s while pending.
|
|
5
|
+
//
|
|
1
6
|
// The permission broker's two doors (6.52.0). See `lib/permission-broker.ts` for the why.
|
|
2
7
|
//
|
|
3
8
|
// POST /api/permission-requests/ask -- the COS session hook, on a PermissionRequest at a
|
|
@@ -35,7 +40,6 @@ import { timingSafeTokenEqual } from '../lib/token-auth.js'
|
|
|
35
40
|
import {
|
|
36
41
|
ANSWERING_CLIENTS,
|
|
37
42
|
CLIENT_LIVE_WINDOW_MS,
|
|
38
|
-
QUESTIONS_POLL_INTERVAL_MS,
|
|
39
43
|
SESSION_QUESTIONS_PROTOCOL_VERSION,
|
|
40
44
|
type BrokerAdmission,
|
|
41
45
|
type HookReplyChannel,
|
|
@@ -182,7 +186,7 @@ export function createSessionQuestionsRouter(deps: SessionQuestionsRouterDeps):
|
|
|
182
186
|
enabled: health.enabled,
|
|
183
187
|
mode: health.mode,
|
|
184
188
|
protocolVersion: SESSION_QUESTIONS_PROTOCOL_VERSION,
|
|
185
|
-
pollIntervalMs:
|
|
189
|
+
pollIntervalMs: deps.broker.questionsPollIntervalMs(),
|
|
186
190
|
liveWindowMs: CLIENT_LIVE_WINDOW_MS,
|
|
187
191
|
pending: deps.broker.pendingCount(),
|
|
188
192
|
stats: deps.broker.stats(),
|
package/shared/meeting-ask.ts
CHANGED
|
@@ -163,6 +163,8 @@ export interface MeetingAskExchange {
|
|
|
163
163
|
sections?: MeetingAskSection[];
|
|
164
164
|
sources: MeetingAskSource[];
|
|
165
165
|
notices: string[];
|
|
166
|
+
/** Audited public query actually admitted for this answer, if any. */
|
|
167
|
+
publicWebQuery?: string;
|
|
166
168
|
throughMs: number;
|
|
167
169
|
files: MeetingAskFile[];
|
|
168
170
|
error?: string;
|
|
@@ -173,6 +175,7 @@ export interface MeetingAskCapabilities {
|
|
|
173
175
|
durableHistory: true;
|
|
174
176
|
attachmentContext: true;
|
|
175
177
|
webRetrieval: boolean;
|
|
178
|
+
publicTopics?: true;
|
|
176
179
|
/** 6.71.1: POST /api/meeting-ask/files/media turns uploaded media into meeting files. */
|
|
177
180
|
phoneFiles?: true;
|
|
178
181
|
/** 6.71.1: config carries cosSearch and PUT config {cosSearch} changes it. */
|
|
@@ -191,6 +194,8 @@ export interface MeetingAskConfig {
|
|
|
191
194
|
selected: MeetingAskModel;
|
|
192
195
|
/** 6.71.1. Missing on older servers, which always used 'meaning'. */
|
|
193
196
|
cosSearch?: MeetingAskCosSearch;
|
|
197
|
+
/** Explicit opt-in to public-topic selection from recent transcript for fact checks. */
|
|
198
|
+
publicTopics?: boolean;
|
|
194
199
|
models: Array<MeetingAskModel & {
|
|
195
200
|
available: boolean;
|
|
196
201
|
reason?: string;
|
|
@@ -209,6 +214,8 @@ export interface MeetingAskConfig {
|
|
|
209
214
|
* PUT files {sessionId,serverInstanceId,files:MeetingAskFile[]} -> {files}. Upsert only; removed is explicit.
|
|
210
215
|
* POST files/media {sessionId,serverInstanceId,media:[{id,name?}]} -> {files} (6.71.1, capabilities.phoneFiles):
|
|
211
216
|
* media already uploaded through /api/media joins the meeting's file list as id "phone_<mediaId>", origin "phone".
|
|
217
|
+
* 6.71.9 on macOS: images containing text use local OCR and revisioned F evidence; no-word images
|
|
218
|
+
* remain unsupported and extraction errors are explicit failed rows. Historical evidence is immutable.
|
|
212
219
|
* GET export?sessionId=...&serverInstanceId=... -> {text}; composes persisted Ask/file context,
|
|
213
220
|
* client prepends its current canonical transcript. All operations use X-Cos-Token.
|
|
214
221
|
* GET config is the capability probe (404 means old server); health.capabilities.meetingAsk mirrors capabilities.
|