@gotcos/glasses-server 6.76.1 → 6.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +40 -0
- package/CHANGELOG.md +56 -0
- package/README.md +12 -1
- package/managed-runtime-contract.json +17 -1
- package/package.json +10 -1
- package/server/index.ts +98 -10
- package/server/lib/agent-session-binding-registry.ts +623 -9
- package/server/lib/boot-resume.ts +552 -0
- package/server/lib/claude-bridge.ts +152 -3
- package/server/lib/claude-cli-support.ts +39 -0
- package/server/lib/claude-steer.ts +259 -0
- package/server/lib/cos-bridge-notes.ts +73 -0
- package/server/lib/decision-provider.ts +9 -0
- package/server/lib/env-switch.ts +17 -0
- package/server/lib/fuzzy-correct.ts +77 -49
- package/server/lib/live-cos-ask.ts +41 -7
- package/server/lib/live-cues-v2-notes.ts +152 -0
- package/server/lib/live-cues-v2-settings.ts +63 -0
- package/server/lib/live-cues-v2.ts +198 -38
- package/server/lib/managed-restart.ts +8 -0
- package/server/lib/provider-actions.ts +18 -0
- package/server/lib/query-job-coordinator.ts +300 -0
- package/server/lib/query-job-feature.ts +39 -0
- package/server/lib/query-job-resume.ts +269 -0
- package/server/lib/query-job-runtime.ts +23 -3
- package/server/lib/query-job-store.ts +516 -7
- package/server/lib/query-job-types.ts +210 -1
- package/server/lib/queue-turn-evidence.ts +213 -0
- package/server/lib/thread-turn-queue-deliver.ts +6 -1
- package/server/lib/thread-turn-queue-store.ts +13 -4
- package/server/lib/thread-turn-queue.ts +151 -1
- package/server/lib/transcript-cleanup-context.ts +234 -0
- package/server/lib/transcript-cleanup-ledger.ts +304 -0
- package/server/lib/transcript-cleanup-model.ts +217 -0
- package/server/lib/transcript-cleanup-screen.ts +249 -0
- package/server/lib/transcript-cleanup-spans.ts +358 -0
- package/server/lib/transcript-cleanup-state.ts +201 -0
- package/server/lib/transcript-cleanup-suggestions.ts +126 -0
- package/server/lib/transcript-cleanup-text.ts +200 -0
- package/server/lib/transcript-cleanup.ts +427 -0
- package/server/routes/agent-session-bindings.ts +169 -11
- package/server/routes/glossary.ts +38 -19
- package/server/routes/health.ts +29 -1
- package/server/routes/live-cues.ts +32 -3
- package/server/routes/maintenance.ts +34 -0
- package/server/routes/meeting.ts +92 -9
- package/server/routes/query-jobs.ts +107 -13
- package/server/routes/recovery.ts +4 -0
- package/server/routes/thread-turn-queue.ts +371 -10
- package/server/routes/transcribe.ts +3 -0
- package/server/routes/transcript-cleanup.ts +237 -0
- package/shared/live-cues-v2.ts +9 -4
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Transcript cleanup routes (6.77.0). Token-authenticated by the global /api X-Cos-Token
|
|
2
|
+
// middleware (index.ts) like every other /api route; nothing here is public.
|
|
3
|
+
//
|
|
4
|
+
// Settings (COS Control's toggle, later):
|
|
5
|
+
// GET /transcript-cleanup/settings { enabled, source, model, modelSource, limits, today }
|
|
6
|
+
// POST /transcript-cleanup/settings { enabled?, model? } same shape; 409 env_forced when env pins the value
|
|
7
|
+
// Screen context (COS Control supplies on-screen text during a meeting, later):
|
|
8
|
+
// POST /meeting/:sessionId/screen-context { lines: [{ at, text, app? }] } counts of what was stored
|
|
9
|
+
// GET /meeting/:sessionId/screen-context counts only, never the text
|
|
10
|
+
// DELETE /meeting/:sessionId/screen-context { deleted }
|
|
11
|
+
// The cleanup ledger of one meeting:
|
|
12
|
+
// GET /meeting/:sessionId/transcript-cleanup { runs, edits } (edits carry original and replacement words)
|
|
13
|
+
// POST /meeting/:sessionId/transcript-cleanup/undo { editId } | { runId }
|
|
14
|
+
// Dictionary suggestions (COS Control's Dictionary tab, later):
|
|
15
|
+
// GET /transcript-cleanup/suggestions?status=pending|all
|
|
16
|
+
// POST /transcript-cleanup/suggestions/:id/accept { correction? }
|
|
17
|
+
// POST /transcript-cleanup/suggestions/:id/dismiss
|
|
18
|
+
|
|
19
|
+
import { Router } from 'express'
|
|
20
|
+
import { appendScreenContext, deleteScreenContext, ScreenContextError, SESSION_ID, screenContextStats } from '../lib/transcript-cleanup-screen.js'
|
|
21
|
+
import { alreadyCleaned, ledgerView, readLedger, readTranscriptSection, sha256, undoCleanupEdits } from '../lib/transcript-cleanup-ledger.js'
|
|
22
|
+
import { cleanupLimits, resolveCleanupModel, resolveCleanupSwitch, transcriptCleanupBudget, transcriptCleanupSettings, type TranscriptCleanupBudget, type TranscriptCleanupSettings } from '../lib/transcript-cleanup-state.js'
|
|
23
|
+
import { SUGGESTION_ID, suggestionStore, type DictionarySuggestion, type SuggestionStore } from '../lib/transcript-cleanup-suggestions.js'
|
|
24
|
+
import { getVocabulary } from '../lib/profile.js'
|
|
25
|
+
import { readFileSync } from 'node:fs'
|
|
26
|
+
|
|
27
|
+
export const TRANSCRIPT_CLEANUP_PROTOCOL = 1
|
|
28
|
+
|
|
29
|
+
type GlossaryOutcome = { status: number; body: unknown }
|
|
30
|
+
|
|
31
|
+
export interface TranscriptCleanupRouteDeps {
|
|
32
|
+
env?: NodeJS.ProcessEnv
|
|
33
|
+
now?: () => Date
|
|
34
|
+
settings?: TranscriptCleanupSettings
|
|
35
|
+
budget?: TranscriptCleanupBudget
|
|
36
|
+
suggestions?: SuggestionStore
|
|
37
|
+
/** 'active' while the meeting is being recorded. */
|
|
38
|
+
sessionState?: (sessionId: string) => 'active' | 'closed' | 'missing'
|
|
39
|
+
sessionStartedAt?: (sessionId: string) => number | null
|
|
40
|
+
/** The saved meeting's files, or null. */
|
|
41
|
+
findMeeting?: (sessionId: string) => { meetingPath: string; sidecarPath: string } | null
|
|
42
|
+
addVocabulary?: (term: string) => Promise<GlossaryOutcome>
|
|
43
|
+
learnCorrection?: (body: { heard: string; written: string; source: 'meeting' }) => Promise<GlossaryOutcome>
|
|
44
|
+
vocabulary?: () => string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Health and capability shape. Codes only: /api/health is public. */
|
|
48
|
+
export function transcriptCleanupAdvertised(env: NodeJS.ProcessEnv = process.env, settings: TranscriptCleanupSettings = transcriptCleanupSettings()): { protocol: number; enabled: boolean } {
|
|
49
|
+
let enabled = false
|
|
50
|
+
try { enabled = resolveCleanupSwitch(env, settings).enabled } catch { enabled = false }
|
|
51
|
+
return { protocol: TRANSCRIPT_CLEANUP_PROTOCOL, enabled }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function suggestionView(s: DictionarySuggestion, vocabulary: string[]): DictionarySuggestion & { inVocabulary: boolean } {
|
|
55
|
+
const lower = s.written.trim().toLowerCase()
|
|
56
|
+
return { ...s, inVocabulary: vocabulary.some(v => v.trim().toLowerCase() === lower) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createTranscriptCleanupRouter(deps: TranscriptCleanupRouteDeps = {}): Router {
|
|
60
|
+
const env = deps.env ?? process.env
|
|
61
|
+
const now = deps.now ?? (() => new Date())
|
|
62
|
+
const settings = deps.settings ?? transcriptCleanupSettings()
|
|
63
|
+
const budget = deps.budget ?? transcriptCleanupBudget()
|
|
64
|
+
const suggestions = deps.suggestions ?? suggestionStore()
|
|
65
|
+
const sessionState = deps.sessionState ?? (() => 'missing' as const)
|
|
66
|
+
const sessionStartedAt = deps.sessionStartedAt ?? (() => null)
|
|
67
|
+
const findMeeting = deps.findMeeting ?? (() => null)
|
|
68
|
+
const vocabulary = deps.vocabulary ?? (() => { try { return getVocabulary() } catch { return [] } })
|
|
69
|
+
const addVocabulary = deps.addVocabulary ?? (async term => (await import('./glossary.js')).glossaryAddVocabulary(term))
|
|
70
|
+
const learnCorrection = deps.learnCorrection ?? (async body => (await import('./glossary.js')).glossaryLearnCorrection(body))
|
|
71
|
+
const router = Router()
|
|
72
|
+
|
|
73
|
+
const settingsView = () => {
|
|
74
|
+
const sw = resolveCleanupSwitch(env, settings)
|
|
75
|
+
const model = resolveCleanupModel(env, settings)
|
|
76
|
+
const limits = cleanupLimits(env)
|
|
77
|
+
const state = budget.read()
|
|
78
|
+
return {
|
|
79
|
+
protocol: TRANSCRIPT_CLEANUP_PROTOCOL,
|
|
80
|
+
enabled: sw.enabled,
|
|
81
|
+
source: sw.source,
|
|
82
|
+
model: model.choice,
|
|
83
|
+
modelId: model.id,
|
|
84
|
+
modelSource: model.source,
|
|
85
|
+
limits: { dailyCalls: limits.dailyCalls, dailyTokens: limits.dailyTokens, maxSpansPerMeeting: limits.maxSpans },
|
|
86
|
+
today: {
|
|
87
|
+
calls: Math.min(state.calls, Number.MAX_SAFE_INTEGER),
|
|
88
|
+
tokens: Math.min(state.tokens, Number.MAX_SAFE_INTEGER),
|
|
89
|
+
breakerOpenUntil: state.breakerOpenUntil !== null && state.breakerOpenUntil > now().getTime() ? new Date(state.breakerOpenUntil).toISOString() : null,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
router.get('/transcript-cleanup/settings', (_req, res) => {
|
|
95
|
+
res.set('Cache-Control', 'private, no-store')
|
|
96
|
+
try { return res.json(settingsView()) } catch { return res.status(503).json({ error: 'settings_unavailable' }) }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
router.post('/transcript-cleanup/settings', (req, res) => {
|
|
100
|
+
res.set('Cache-Control', 'private, no-store')
|
|
101
|
+
const body = req.body as { enabled?: unknown; model?: unknown } | undefined
|
|
102
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return res.status(400).json({ error: 'invalid_request' })
|
|
103
|
+
const hasEnabled = body.enabled !== undefined, hasModel = body.model !== undefined
|
|
104
|
+
if ((!hasEnabled && !hasModel) || (hasEnabled && typeof body.enabled !== 'boolean') || (hasModel && body.model !== 'haiku' && body.model !== 'sonnet')) {
|
|
105
|
+
return res.status(400).json({ error: 'invalid_request' })
|
|
106
|
+
}
|
|
107
|
+
if (hasEnabled && (env.COS_TRANSCRIPT_CLEANUP === '1' || env.COS_TRANSCRIPT_CLEANUP === '0') && body.enabled !== (env.COS_TRANSCRIPT_CLEANUP === '1')) {
|
|
108
|
+
return res.status(409).json({ error: 'env_forced' })
|
|
109
|
+
}
|
|
110
|
+
if (hasModel && (env.COS_TRANSCRIPT_CLEANUP_MODEL === 'haiku' || env.COS_TRANSCRIPT_CLEANUP_MODEL === 'sonnet') && body.model !== env.COS_TRANSCRIPT_CLEANUP_MODEL) {
|
|
111
|
+
return res.status(409).json({ error: 'env_forced' })
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
settings.save({ ...(hasEnabled ? { enabled: body.enabled as boolean } : {}), ...(hasModel ? { model: body.model as 'haiku' | 'sonnet' } : {}) }, now())
|
|
115
|
+
return res.json(settingsView())
|
|
116
|
+
} catch {
|
|
117
|
+
return res.status(500).json({ error: 'settings_write_failed' })
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const sessionParam = (raw: string | undefined): string | null => (raw && SESSION_ID.test(raw) ? raw : null)
|
|
122
|
+
|
|
123
|
+
/** A late flush right after stop is welcome until the cleanup has run on the saved meeting. */
|
|
124
|
+
function screenAdmission(sessionId: string): { ok: true } | { ok: false; status: number; error: string } {
|
|
125
|
+
if (sessionState(sessionId) === 'active') return { ok: true }
|
|
126
|
+
const meeting = findMeeting(sessionId)
|
|
127
|
+
if (!meeting) return { ok: false, status: 404, error: 'unknown_session' }
|
|
128
|
+
const done = readLedger(sessionId).rows.some(r => r.kind === 'run' && (r.phase === 'done' || r.phase === 'skipped'))
|
|
129
|
+
return done ? { ok: false, status: 409, error: 'cleanup_done' } : { ok: true }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
router.post('/meeting/:sessionId/screen-context', (req, res) => {
|
|
133
|
+
res.set('Cache-Control', 'private, no-store')
|
|
134
|
+
const sessionId = sessionParam(req.params.sessionId)
|
|
135
|
+
if (!sessionId) return res.status(400).json({ error: 'invalid_session_id' })
|
|
136
|
+
const admission = screenAdmission(sessionId)
|
|
137
|
+
if (!admission.ok) return res.status(admission.status).json({ error: admission.error })
|
|
138
|
+
try {
|
|
139
|
+
const result = appendScreenContext(sessionId, req.body, { now: now().getTime(), sessionStartedAt: sessionStartedAt(sessionId) })
|
|
140
|
+
return res.json({ ok: true, protocol: TRANSCRIPT_CLEANUP_PROTOCOL, ...result })
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof ScreenContextError) return res.status(error.status).json({ error: error.code })
|
|
143
|
+
return res.status(503).json({ error: 'screen_context_unavailable' })
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
router.get('/meeting/:sessionId/screen-context', (req, res) => {
|
|
148
|
+
res.set('Cache-Control', 'private, no-store')
|
|
149
|
+
const sessionId = sessionParam(req.params.sessionId)
|
|
150
|
+
if (!sessionId) return res.status(400).json({ error: 'invalid_session_id' })
|
|
151
|
+
return res.json({ protocol: TRANSCRIPT_CLEANUP_PROTOCOL, ...screenContextStats(sessionId) })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
router.delete('/meeting/:sessionId/screen-context', (req, res) => {
|
|
155
|
+
res.set('Cache-Control', 'private, no-store')
|
|
156
|
+
const sessionId = sessionParam(req.params.sessionId)
|
|
157
|
+
if (!sessionId) return res.status(400).json({ error: 'invalid_session_id' })
|
|
158
|
+
try { return res.json({ deleted: deleteScreenContext(sessionId) }) } catch { return res.status(500).json({ error: 'delete_failed' }) }
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
router.get('/meeting/:sessionId/transcript-cleanup', (req, res) => {
|
|
162
|
+
res.set('Cache-Control', 'private, no-store')
|
|
163
|
+
const sessionId = sessionParam(req.params.sessionId)
|
|
164
|
+
if (!sessionId) return res.status(400).json({ error: 'invalid_session_id' })
|
|
165
|
+
const view = ledgerView(sessionId)
|
|
166
|
+
let current: string | null = null
|
|
167
|
+
const meeting = findMeeting(sessionId)
|
|
168
|
+
if (meeting) {
|
|
169
|
+
try { const section = readTranscriptSection(readFileSync(meeting.meetingPath, 'utf8')); current = section === null ? null : sha256(section) } catch { current = null }
|
|
170
|
+
}
|
|
171
|
+
return res.json({ protocol: TRANSCRIPT_CLEANUP_PROTOCOL, ...view, cleanedCurrent: current !== null && alreadyCleaned(sessionId, current) })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
router.post('/meeting/:sessionId/transcript-cleanup/undo', (req, res) => {
|
|
175
|
+
res.set('Cache-Control', 'private, no-store')
|
|
176
|
+
const sessionId = sessionParam(req.params.sessionId)
|
|
177
|
+
if (!sessionId) return res.status(400).json({ error: 'invalid_session_id' })
|
|
178
|
+
const body = (req.body ?? {}) as { editId?: unknown; runId?: unknown }
|
|
179
|
+
const editId = typeof body.editId === 'string' && /^te_[0-9a-f]{16}$/.test(body.editId) ? body.editId : undefined
|
|
180
|
+
const runId = typeof body.runId === 'string' && /^tc_[0-9a-f]{16}$/.test(body.runId) ? body.runId : undefined
|
|
181
|
+
if (!editId === !runId) return res.status(400).json({ error: 'editId_or_runId_required' })
|
|
182
|
+
const meeting = findMeeting(sessionId)
|
|
183
|
+
if (!meeting) return res.status(404).json({ error: 'meeting_missing' })
|
|
184
|
+
const result = undoCleanupEdits({ sessionId, meetingPath: meeting.meetingPath, sidecarPath: meeting.sidecarPath, editId, runId, now })
|
|
185
|
+
if (!result.ok) return res.status(result.status).json({ error: result.error })
|
|
186
|
+
return res.json({ reverted: result.reverted })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
router.get('/transcript-cleanup/suggestions', (req, res) => {
|
|
190
|
+
res.set('Cache-Control', 'private, no-store')
|
|
191
|
+
const status = typeof req.query.status === 'string' ? req.query.status : 'pending'
|
|
192
|
+
if (status !== 'pending' && status !== 'all') return res.status(400).json({ error: 'invalid_status' })
|
|
193
|
+
const vocab = vocabulary()
|
|
194
|
+
const list = suggestions.list().filter(s => status === 'all' || s.status === 'pending')
|
|
195
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map(s => suggestionView(s, vocab))
|
|
196
|
+
return res.json({ protocol: TRANSCRIPT_CLEANUP_PROTOCOL, suggestions: list })
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
router.post('/transcript-cleanup/suggestions/:id/accept', async (req, res) => {
|
|
200
|
+
res.set('Cache-Control', 'private, no-store')
|
|
201
|
+
const id = req.params.id
|
|
202
|
+
if (!SUGGESTION_ID.test(id)) return res.status(400).json({ error: 'invalid_suggestion_id' })
|
|
203
|
+
const body = (req.body ?? {}) as { correction?: unknown }
|
|
204
|
+
if (body.correction !== undefined && typeof body.correction !== 'boolean') return res.status(400).json({ error: 'invalid_request' })
|
|
205
|
+
const suggestion = suggestions.get(id)
|
|
206
|
+
if (!suggestion) return res.status(404).json({ error: 'suggestion_not_found' })
|
|
207
|
+
if (suggestion.status === 'dismissed') return res.status(409).json({ error: 'suggestion_dismissed' })
|
|
208
|
+
try {
|
|
209
|
+
const heard = suggestion.heard.find(h => h.trim().toLowerCase() !== suggestion.written.trim().toLowerCase())
|
|
210
|
+
const outcome = body.correction === true && heard
|
|
211
|
+
? await learnCorrection({ heard, written: suggestion.written, source: 'meeting' })
|
|
212
|
+
: await addVocabulary(suggestion.written)
|
|
213
|
+
if (outcome.status !== 200) return res.status(outcome.status).json(outcome.body)
|
|
214
|
+
const accepted = suggestions.setStatus(id, 'accepted', now().toISOString()) ?? suggestion
|
|
215
|
+
const out = outcome.body as { unchanged?: boolean; glossary?: unknown; entry?: unknown }
|
|
216
|
+
return res.json({ suggestion: suggestionView(accepted, vocabulary()), unchanged: out.unchanged === true, glossary: out.glossary ?? null, entry: out.entry ?? null })
|
|
217
|
+
} catch {
|
|
218
|
+
return res.status(500).json({ error: 'accept_failed' })
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
router.post('/transcript-cleanup/suggestions/:id/dismiss', (req, res) => {
|
|
223
|
+
res.set('Cache-Control', 'private, no-store')
|
|
224
|
+
const id = req.params.id
|
|
225
|
+
if (!SUGGESTION_ID.test(id)) return res.status(400).json({ error: 'invalid_suggestion_id' })
|
|
226
|
+
try {
|
|
227
|
+
const dismissed = suggestions.setStatus(id, 'dismissed', now().toISOString())
|
|
228
|
+
if (!dismissed) return res.status(404).json({ error: 'suggestion_not_found' })
|
|
229
|
+
return res.json({ suggestion: suggestionView(dismissed, vocabulary()) })
|
|
230
|
+
} catch {
|
|
231
|
+
return res.status(500).json({ error: 'dismiss_failed' })
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return router
|
|
236
|
+
}
|
|
237
|
+
|
package/shared/live-cues-v2.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Live cues v2 wire contract (server 6.75 candidate, companion 6.10.641 candidate).
|
|
2
|
+
* Live cues v2 wire contract (server 6.75 candidate, companion 6.10.641 candidate; optional capability fields since 6.76.2).
|
|
3
3
|
* Additive to v1: old clients keep receiving `coaching_nudge` and ignore the new named event.
|
|
4
4
|
* The companion keeps a byte-identical copy at src/lib/live-cues-v2-contract.ts.
|
|
5
5
|
*/
|
|
@@ -33,13 +33,18 @@ export interface LiveCueV2 {
|
|
|
33
33
|
/** v1 `coaching_nudge` as the v2 engine emits it for old clients (headline only). */
|
|
34
34
|
export interface LiveCueV1Compat { nudge: string; type: CueKind; priority: 2; cueId: string; sessionId: string; protocol: 2 }
|
|
35
35
|
|
|
36
|
-
/** `reason` when unavailable: master_flag_off, api_key_in_env,
|
|
37
|
-
*
|
|
38
|
-
|
|
36
|
+
/** `reason` when unavailable: master_flag_off, api_key_in_env, decision_invalid_provider, decision_not_configured (no key
|
|
37
|
+
* for the selected decision provider; jev_not_configured when Jev was chosen and only a SubQ key exists), claude_not_found,
|
|
38
|
+
* budget_exhausted, jev_cap_reached, jev_breaker_open, decision_cap_reached, decision_breaker_open (the last two for SubQ).
|
|
39
|
+
* Server 6.76.2 adds two optional fields; older servers omit them. `memory`: where past notes come from (cue_memory.py,
|
|
40
|
+
* the owner's own COS notes and meetings, or nothing). `decisionProvider`: which classifier runs the gate and the judge. */
|
|
41
|
+
export type LiveCuesV2MemorySource = 'cue_memory' | 'notes' | 'none'
|
|
42
|
+
export interface LiveCuesV2Capability { available: boolean; reason?: string; protocol: 2; model: string; memory?: LiveCuesV2MemorySource; decisionProvider?: 'jev' | 'subq' }
|
|
39
43
|
|
|
40
44
|
/** Why cues are not coming for this meeting. `active` means evaluations are running normally (silence is a choice). */
|
|
41
45
|
export type LiveCuesV2SessionState = 'active' | 'paused' | 'ended' | 'stopped'
|
|
42
46
|
export type LiveCuesV2StopReason = 'breaker_open' | 'meeting_cap' | 'budget_exhausted' | 'jev_cap_reached' | 'jev_breaker_open' | 'jev_not_configured'
|
|
47
|
+
| 'decision_cap_reached' | 'decision_breaker_open' | 'decision_not_configured' | 'decision_invalid_provider'
|
|
43
48
|
|
|
44
49
|
export interface LiveCuesV2SessionStatus {
|
|
45
50
|
sessionId: string
|