@gotcos/glasses-server 6.75.0 → 6.77.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/CHANGELOG.md +63 -0
- package/managed-runtime-contract.json +8 -0
- package/package.json +1 -1
- package/server/index.ts +6 -1
- package/server/lib/cos-bridge-notes.ts +73 -0
- package/server/lib/decision-provider.ts +9 -0
- package/server/lib/display-bus.ts +2 -0
- package/server/lib/fuzzy-correct.ts +77 -49
- package/server/lib/live-cos-ask.ts +118 -21
- 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/meeting-ask-owner.ts +144 -0
- package/server/lib/meeting-ask-runtime.ts +28 -6
- package/server/lib/meeting-stop-requests.ts +123 -0
- 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/glossary.ts +38 -19
- package/server/routes/health.ts +3 -0
- package/server/routes/live-cues.ts +32 -3
- package/server/routes/meeting-ask.ts +11 -1
- package/server/routes/meeting.ts +177 -0
- package/server/routes/transcribe.ts +3 -0
- package/server/routes/transcript-cleanup.ts +237 -0
- package/server/scripts/live-cues-v2-notes-mutation-gate.mjs +170 -0
- package/server/scripts/transcript-cleanup-mutation-gate.mjs +219 -0
- package/shared/live-cues-v2.ts +9 -4
- package/shared/meeting-ask.ts +23 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Live cues v2 6.76.2 mutation gate: the runtime switch, the notes fallback and the decision provider. Mutations run only
|
|
3
|
+
// in a disposable copy of the checkout; the live checkout is never rewritten.
|
|
4
|
+
//
|
|
5
|
+
// What it proves, in order:
|
|
6
|
+
// 0. The target suites are GREEN unmutated. A gate over a red suite reads every mutant as killed and proves nothing.
|
|
7
|
+
// 1. Each target string appears EXACTLY ONCE in its file, so the mutation is unambiguous.
|
|
8
|
+
// 2. The mutated file differs from the original, so the edit landed.
|
|
9
|
+
// 3. A NAMED test fails ("FAIL server/x.test.ts > name"); a build error or a harness failure is not a kill.
|
|
10
|
+
// 4. The file is restored byte for byte (compared) before the next mutant.
|
|
11
|
+
// Also the Live COS Ask's memory bridge fallback (searchAskHistory) and the helpers it shares with cues (cos-bridge-notes.ts).
|
|
12
|
+
// Usage: node server/scripts/live-cues-v2-notes-mutation-gate.mjs [--list] [name…]
|
|
13
|
+
import { mkdtempSync, cpSync, readFileSync, writeFileSync, symlinkSync, rmSync } from 'node:fs'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { resolve, join, dirname, basename } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { spawnSync } from 'node:child_process'
|
|
18
|
+
|
|
19
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
20
|
+
const CUES = ['server/lib/live-cues-v2.test.ts', 'server/lib/live-cues-v2-notes.test.ts', 'server/lib/live-cues-v2-notes-bridge.test.ts',
|
|
21
|
+
'server/lib/live-cues-v2-settings.test.ts', 'server/routes/live-cues-v2-settings-route.test.ts', 'server/routes/live-cues-v2-route.test.ts']
|
|
22
|
+
const HEALTH = [...CUES, 'server/routes/health.test.ts']
|
|
23
|
+
const ASK = ['server/lib/live-cos-ask-memory-fallback.test.ts', 'server/lib/live-cos-ask-queen-runtime.test.ts', 'server/lib/meeting-ask-cos-search.test.ts',
|
|
24
|
+
'server/lib/live-cos-ask.test.ts', 'server/lib/meeting-ask-owner.test.ts']
|
|
25
|
+
const BOTH = [...CUES, ...ASK]
|
|
26
|
+
const ALL = [...HEALTH, ...ASK]
|
|
27
|
+
const ENGINE = 'server/lib/live-cues-v2.ts', NOTES = 'server/lib/live-cues-v2-notes.ts', SETTINGS = 'server/lib/live-cues-v2-settings.ts'
|
|
28
|
+
const ROUTE = 'server/routes/live-cues.ts', DECISION = 'server/lib/decision-provider.ts'
|
|
29
|
+
const SHARED = 'server/lib/cos-bridge-notes.ts', ASKLIB = 'server/lib/live-cos-ask.ts'
|
|
30
|
+
const m = (name, file, edits, tests = CUES) => ({ name, file, edits: typeof edits[0] === 'string' ? [edits] : edits, tests })
|
|
31
|
+
|
|
32
|
+
const mutations = [
|
|
33
|
+
// ── Runtime switch ──
|
|
34
|
+
m('env-forces-only-exact-1', SETTINGS, ["return env.COS_LIVE_CUES_V2 === '1'", 'return !!env.COS_LIVE_CUES_V2']),
|
|
35
|
+
m('env-forced-wins-over-setting', SETTINGS, ["if (liveCuesV2EnvForced(env)) return { enabled: true, source: 'env' }", 'if (false) return { enabled: true, source: \'env\' }']),
|
|
36
|
+
m('saved-setting-read', SETTINGS, ["if (stored !== null) return { enabled: stored, source: 'setting' }", "if (stored === true) return { enabled: stored, source: 'setting' }"]),
|
|
37
|
+
m('malformed-setting-is-none', SETTINGS, ["typeof (raw as { enabled?: unknown }).enabled === 'boolean'", '(raw as { enabled?: unknown }).enabled !== undefined']),
|
|
38
|
+
m('cache-follows-write', SETTINGS, [' this.cache = { value: enabled }\n', '']),
|
|
39
|
+
m('failed-write-keeps-memory', SETTINGS, [
|
|
40
|
+
[' mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 })\n', ' this.cache = { value: enabled }\n mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 })\n'],
|
|
41
|
+
["{ mode: 0o600 })\n this.cache = { value: enabled }\n", '{ mode: 0o600 })\n']]),
|
|
42
|
+
m('settings-file-private', SETTINGS, ["+ '\\n', { mode: 0o600 })", "+ '\\n', { mode: 0o644 })"]),
|
|
43
|
+
m('engine-reads-setting', ENGINE, ['private enabled(): boolean { return liveCuesV2Enabled(this.deps.env, this.deps.settings) }', 'private enabled(): boolean { return liveCuesV2Enabled(this.deps.env, null) }']),
|
|
44
|
+
m('feed-stops-while-off', ENGINE, [' if (!this.enabled()) return\n if (this.deps.store.isEnded(sessionId)) return', ' if (this.deps.store.isEnded(sessionId)) return']),
|
|
45
|
+
m('last-check-before-emit', ENGINE, ['|| this.deps.store.isPaused(s.sessionId) || !this.enabled()) {', '|| this.deps.store.isPaused(s.sessionId)) {']),
|
|
46
|
+
m('suspend-aborts-in-flight', ENGINE, ['if (s.inFlight) { s.inFlight.abort(V2_OFF); aborted += 1 }', 'if (s.inFlight) { aborted += 1 }']),
|
|
47
|
+
m('cancel-reason-traced', ENGINE, [" if (trace.outcome === 'cancelled' && abort.signal.reason === V2_OFF) trace.cancelReason = V2_OFF\n", '']),
|
|
48
|
+
m('env-forced-off-refused', ENGINE, ["if (!enabled && liveCuesV2EnvForced(env)) return { ok: false, error: 'env_forced' }", "if (false) return { ok: false, error: 'env_forced' }"]),
|
|
49
|
+
m('turning-off-suspends', ENGINE, ['const aborted = engine?.suspend() ?? 0', 'const aborted = 0']),
|
|
50
|
+
m('suspend-only-on-transition', ENGINE, ['if (wasOn && !liveCuesV2Enabled(env)) {', 'if (!liveCuesV2Enabled(env)) {']),
|
|
51
|
+
m('saved-meeting-ends-known-session', ENGINE, ['(liveCuesV2Enabled() || engine?.knows(sid))', 'liveCuesV2Enabled()']),
|
|
52
|
+
m('knows-armed-sessions', ENGINE, ['knows(sessionId: string): boolean { return this.sessions.has(sessionId) }', 'knows(sessionId: string): boolean { return false }']),
|
|
53
|
+
m('route-v2-owns-known-meeting', ROUTE, ['return liveCuesV2Enabled() || (peekLiveCuesV2()?.knows(sessionId) ?? false)', 'return liveCuesV2Enabled()']),
|
|
54
|
+
m('route-boolean-enabled-only', ROUTE, ["typeof body.enabled !== 'boolean'", 'body.enabled === undefined']),
|
|
55
|
+
m('route-409-body', ROUTE, ['if (!result.ok) return res.status(409).json({ error: result.error })', 'if (!result.ok) return res.status(200).json(liveCuesV2SettingsView())']),
|
|
56
|
+
m('advertised-follows-setting', ENGINE, ['return liveCuesV2Enabled(env) ? liveCuesV2Capability() : undefined', "return env.COS_LIVE_CUES_V2 === '1' ? liveCuesV2Capability() : undefined"], HEALTH),
|
|
57
|
+
// ── Notes fallback (engine) ──
|
|
58
|
+
m('fallback-runs-on-failure', ENGINE, ['if (cueMemoryFailed(mem) && !abort.signal.aborted) {', 'if (false) {']),
|
|
59
|
+
m('cue-memory-preferred', ENGINE, ['if (cueMemoryFailed(mem) && !abort.signal.aborted) {', 'if (!abort.signal.aborted) {']),
|
|
60
|
+
m('failure-codes-only', ENGINE, ['return !!mem.denseError && CUE_MEMORY_FAILURES.has(mem.denseError)', 'return !!mem.denseError']),
|
|
61
|
+
m('timeout-is-a-failure', ENGINE, ["'no_cue_memory_script', 'memory_timeout', 'spawn_failed'", "'no_cue_memory_script', 'spawn_failed'"]),
|
|
62
|
+
m('notes-candidates-to-judge', ENGINE, [' mem = { notes: found.notes }\n', ' mem = { notes: [] }\n']),
|
|
63
|
+
m('notes-get-session-and-bound', ENGINE, ['this.deps.notes(window, before, abort.signal, s.sessionId)', "this.deps.notes(window, before, abort.signal, '')"]),
|
|
64
|
+
m('source-none-when-empty', ENGINE, ["cueMemorySource: mem.notes.length ? source : 'none'", 'cueMemorySource: source']),
|
|
65
|
+
m('capability-memory-source', ENGINE, [": notesAvailable() ? 'notes' : 'none'", ": 'none'"]),
|
|
66
|
+
// ── Notes fallback (search) ──
|
|
67
|
+
m('notes-date-bound', NOTES, ['!!c && !(c.date && c.date >= before)', '!!c']),
|
|
68
|
+
m('notes-local-day', SHARED, ["return Number.isFinite(ms) ? new Date(ms).toLocaleDateString('en-CA') : ''", "return Number.isFinite(ms) ? v.slice(0, 10) : ''"], BOTH),
|
|
69
|
+
m('notes-drop-live-meeting', NOTES, ['hits.filter(h => !(opts.sessionId && h.sessionId === opts.sessionId))', 'hits']),
|
|
70
|
+
m('notes-budget-race', NOTES, ['new Promise<boolean>(r => { timer = setTimeout(() => r(true), budgetMs) })', 'new Promise<boolean>(() => {})']),
|
|
71
|
+
m('notes-bridge-timeout', NOTES, ['searchers.bridge(local.signal, budgetMs)', 'searchers.bridge(local.signal, 30_000)']),
|
|
72
|
+
m('notes-abort-at-budget', NOTES, [' clearTimeout(timer)\n local.abort()\n', ' clearTimeout(timer)\n']),
|
|
73
|
+
m('notes-aborted-gets-nothing', NOTES, [' if (signal.aborted) return { notes: [] }\n', '']),
|
|
74
|
+
m('notes-no-terms-no-search', NOTES, [" if (!terms.length) return { notes: [], error: 'notes_no_terms' }\n", '']),
|
|
75
|
+
m('notes-queries-short', NOTES, ['terms.slice(0, 4).join', 'terms.join']),
|
|
76
|
+
m('notes-stopwords', NOTES, ['if (term.length < 3 || STOP.has(term) ||', 'if (term.length < 3 ||']),
|
|
77
|
+
m('shared-rank-fusion', SHARED, ['score: (prev?.score ?? 0) + 1 / (RRF_K + rank)', 'score: 1 / (RRF_K + rank)'], BOTH),
|
|
78
|
+
m('shared-fusion-keeps-order-on-ties', SHARED, ['b.score - a.score || a.order - b.order', 'b.score - a.score || b.order - a.order'], BOTH),
|
|
79
|
+
m('notes-top-four', NOTES, ['fused.ranked(top)', 'fused.ranked()']),
|
|
80
|
+
m('shared-bridge-scored', SHARED, [' .filter(x => x.score > 0).sort(', ' .sort('], BOTH),
|
|
81
|
+
m('shared-bridge-needs-content', SHARED, ['if (!id || !content.trim()) continue', 'if (!id) continue'], BOTH),
|
|
82
|
+
m('shared-bridge-lists-memory', SHARED, ["callPython(['memory', '--days'", "callPython(['memory-overview', '--days'"], BOTH),
|
|
83
|
+
m('notes-bridge-searched', NOTES, [' for (const q of queries) add(rankBridgeMemories(rows, q).map(bridgeCandidate))\n', '']),
|
|
84
|
+
m('notes-real-memories-keyword-only', NOTES, ['(await searchMemories({ query, limit: 10, semantic: false, signal })).hits', '[]']),
|
|
85
|
+
m('notes-real-meetings-keyword-only', NOTES, ['(await searchMeetingLibrary({ query, limit: 10, semantic: false, signal })).hits', '[]']),
|
|
86
|
+
// ── Decision provider ──
|
|
87
|
+
m('cue-override-read', ENGINE, [' if (raw !== undefined) {\n', ' if (false) {\n']),
|
|
88
|
+
m('cue-override-validated', ENGINE, [" if (v !== 'jev' && v !== 'subq') throw new DecisionProviderError('decision_invalid_provider')\n", '']),
|
|
89
|
+
m('only-key-subq', ENGINE, [" if (!keys.jev() && keys.subq()) return { provider: 'subq', source: 'only_key' }\n", '']),
|
|
90
|
+
m('only-key-needs-no-jev', ENGINE, ['if (!keys.jev() && keys.subq()) return', 'if (keys.subq()) return']),
|
|
91
|
+
m('global-env-ignored', ENGINE, [' const raw = env.COS_DECISION_PROVIDER_CUES\n', " const raw = env.COS_DECISION_PROVIDER_CUES ?? env.COS_DECISION_PROVIDER\n"]),
|
|
92
|
+
m('global-setting-ignored', ENGINE, [
|
|
93
|
+
["import type { DecisionProvider } from './decision-settings.js'", "import { readDecisionSettings, type DecisionProvider } from './decision-settings.js'"],
|
|
94
|
+
[' const raw = env.COS_DECISION_PROVIDER_CUES\n', ' const raw = env.COS_DECISION_PROVIDER_CUES ?? readDecisionSettings().provider\n']]),
|
|
95
|
+
m('judge-uses-evaluation-provider', ENGINE, ['abort.signal, provider)\n Object.assign(trace, { judgeMs', 'abort.signal, this.deps.decisionProvider())\n Object.assign(trace, { judgeMs']),
|
|
96
|
+
m('gate-uses-provider', ENGINE, ['await this.deps.gate(s.title, window, abort.signal, provider)', "await this.deps.gate(s.title, window, abort.signal, 'jev')"]),
|
|
97
|
+
m('real-deps-route-subq', ENGINE, ["cueDecisionCalls(provider === 'subq' ? subq : jev)", 'cueDecisionCalls(jev)']),
|
|
98
|
+
m('capability-key-check', ENGINE, [" if (!st.configured) return off(provider === 'jev' && this.deps.decisionState('subq').configured ? 'jev_not_configured' : 'decision_not_configured')\n", '']),
|
|
99
|
+
m('jev-alias-only-with-subq-key', ENGINE, ["provider === 'jev' && this.deps.decisionState('subq').configured ? 'jev_not_configured'", "provider === 'jev' ? 'jev_not_configured'"]),
|
|
100
|
+
m('decision-not-configured-reason', ENGINE, ["this.deps.decisionState('subq').configured ? 'jev_not_configured' : 'decision_not_configured'", "this.deps.decisionState('subq').configured ? 'jev_not_configured' : 'jev_not_configured'"]),
|
|
101
|
+
m('capability-selected-provider-state', ENGINE, [' const st = this.deps.decisionState(provider)\n', " const st = this.deps.decisionState('jev')\n"]),
|
|
102
|
+
m('subq-cap-reason', ENGINE, ["off(provider === 'jev' ? 'jev_cap_reached' : 'decision_cap_reached')", "off('jev_cap_reached')"]),
|
|
103
|
+
m('subq-breaker-reason', ENGINE, ["off(provider === 'jev' ? 'jev_breaker_open' : 'decision_breaker_open')", "off('jev_breaker_open')"]),
|
|
104
|
+
m('capability-names-provider', ENGINE, ['...(provider ? { decisionProvider: provider } : {})', '...({})']),
|
|
105
|
+
m('quiet-decision-stops', ENGINE, ["'decision_cap_reached', 'decision_breaker_open', 'decision_not_configured', 'decision_invalid_provider'])", "'decision_breaker_open', 'decision_not_configured', 'decision_invalid_provider'])"]),
|
|
106
|
+
m('quiet-sees-decision-errors', ENGINE, ['if ((error instanceof JevError || error instanceof DecisionProviderError) && QUIET_DECISION_STOPS.has(error.code)) {', 'if (error instanceof JevError && QUIET_DECISION_STOPS.has(error.code)) {']),
|
|
107
|
+
m('decision-error-code-traced', ENGINE, ["trace.error = error instanceof JevError || error instanceof DecisionProviderError ? error.code :", "trace.error = error instanceof JevError ? error.code :"]),
|
|
108
|
+
m('subq-cue-cap-env', ENGINE, ['Number(process.env.COS_LIVE_CUES_SUBQ_DAILY_TOKENS)', 'Number(process.env.COS_SUBQ_DAILY_TOKENS)']),
|
|
109
|
+
m('subq-cue-ledger', ENGINE, ["dataPath('subq-cues-usage.json')", "dataPath('subq-usage.json')"]),
|
|
110
|
+
m('subq-configured-honest', ENGINE, ['return { configured: st.configured, breakerOpen: st.breakerOpenUntil !== null', 'return { configured: true, breakerOpen: st.breakerOpenUntil !== null']),
|
|
111
|
+
m('subq-status-unreadable-is-spent', DECISION, [' let usedToday = dailyCap\n', ' let usedToday = 0\n']),
|
|
112
|
+
m('subq-tracked-for-new-key', DECISION, ['{ configuredSubqClients.add(client); return client }', '{ return client }']),
|
|
113
|
+
// ── Live COS Ask: memory bridge fallback ──
|
|
114
|
+
m('ask-listing-only-when-memory-did-not-answer', ASKLIB, [' const memoryDidNotAnswer = memoryRaw.length === 0 ||', ' const memoryDidNotAnswer = true ||'], ASK),
|
|
115
|
+
m('ask-listing-when-memory-empty', ASKLIB, [' const memoryDidNotAnswer = memoryRaw.length === 0 ||', ' const memoryDidNotAnswer = false ||'], ASK),
|
|
116
|
+
m('ask-listing-when-meaning-failed', ASKLIB, ["|| (memory.status === 'fulfilled' && semantic && !memory.value.semanticAvailable)\n", '\n'], ASK),
|
|
117
|
+
m('ask-listing-meaning-only', ASKLIB, ["(memory.status === 'fulfilled' && semantic && !memory.value.semanticAvailable)", "(memory.status === 'fulfilled' && !memory.value.semanticAvailable)"], ASK),
|
|
118
|
+
m('ask-listing-threshold', ASKLIB, ['x.score >= ASK_HIT_MIN && (!domain', 'x.score > 0 && (!domain'], ASK),
|
|
119
|
+
m('ask-listing-domain', ASKLIB, ['x.score >= ASK_HIT_MIN && (!domain || x.row.domain === domain)', 'x.score >= ASK_HIT_MIN'], ASK),
|
|
120
|
+
m('ask-memory-cap-three', ASKLIB, ['const memorySources = fused.ranked(3)', 'const memorySources = fused.ranked()'], ASK),
|
|
121
|
+
m('ask-listing-notice', ASKLIB, [" if (memorySources.some(s => s.listed)) notices.push('COS also searched the notes in its memory list by keyword.')\n", ''], ASK),
|
|
122
|
+
m('ask-abort-skips-listing', ASKLIB, ['if (memoryDidNotAnswer && !signal?.aborted) {', 'if (memoryDidNotAnswer) {'], ASK),
|
|
123
|
+
m('ask-listing-failure-tolerated', ASKLIB, ['try { listed = await bridge(signal) } catch { /* no listing: the notices above stand */ }', 'listed = await bridge(signal)'], ASK),
|
|
124
|
+
m('ask-source-id-first', ASKLIB, ["({ id: `H${sources.length + 1}`, ...s })", "({ ...s, id: `H${sources.length + 1}` })"], ASK),
|
|
125
|
+
m('ask-listing-cites-content', ASKLIB, ['text: redactSecrets(row.content).slice(0, 1500), memoryId: row.id', "text: '', memoryId: row.id"], ASK),
|
|
126
|
+
m('ask-default-listing-is-the-bridge', ASKLIB, ['bridge: AskBridgeList = s => listBridgeMemories(s)', 'bridge: AskBridgeList = async () => []'], ASK),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
const args = process.argv.slice(2)
|
|
130
|
+
if (args.includes('--list')) { for (const { name } of mutations) console.log(name); process.exit(0) }
|
|
131
|
+
const selected = args.length ? mutations.filter(({ name }) => args.includes(name)) : mutations
|
|
132
|
+
if (args.length && selected.length !== args.length) throw new Error('Unknown mutation name in: ' + args.join(' '))
|
|
133
|
+
if (new Set(mutations.map(x => x.name)).size !== mutations.length) throw new Error('Duplicate mutation name')
|
|
134
|
+
|
|
135
|
+
const scratch = mkdtempSync(join(tmpdir(), 'cues-notes-mutations-'))
|
|
136
|
+
try {
|
|
137
|
+
const skip = new Set(['data', 'models', 'certs', 'node_modules'])
|
|
138
|
+
cpSync(join(root, 'server'), join(scratch, 'server'), { recursive: true, filter: src => !(skip.has(basename(src)) && dirname(src) === join(root, 'server')) })
|
|
139
|
+
for (const dir of ['shared', 'work-task-runtime', 'bin', 'docs', 'memory-runtime']) cpSync(join(root, dir), join(scratch, dir), { recursive: true })
|
|
140
|
+
for (const file of ['vitest.config.ts', 'package.json', 'managed-runtime-contract.json']) cpSync(join(root, file), join(scratch, file))
|
|
141
|
+
symlinkSync(join(root, 'node_modules'), join(scratch, 'node_modules'), 'dir')
|
|
142
|
+
const run = tests => spawnSync(process.execPath, [join(root, 'node_modules/vitest/vitest.mjs'), 'run', '--maxWorkers=1', ...tests], {
|
|
143
|
+
cwd: scratch, env: { PATH: process.env.PATH, HOME: scratch, TMPDIR: process.env.TMPDIR, COS_DATA_DIR: join(scratch, 'data'), COS_PROFILE_PATH: join(scratch, 'data', 'profile.json') },
|
|
144
|
+
encoding: 'utf8', timeout: 300000,
|
|
145
|
+
})
|
|
146
|
+
const baseline = run(ALL)
|
|
147
|
+
if (baseline.status !== 0) throw new Error('Baseline failed; a gate over a red suite proves nothing:\n' + baseline.stdout + '\n' + baseline.stderr)
|
|
148
|
+
const totals = /Tests\s+(\d+) passed/.exec(baseline.stdout)
|
|
149
|
+
console.log(`baseline PASS (${totals ? totals[1] : '?'} tests in ${ALL.length} files)`)
|
|
150
|
+
let killed = 0
|
|
151
|
+
for (const { name, file, edits, tests } of selected) {
|
|
152
|
+
const target = join(scratch, file), original = readFileSync(target, 'utf8')
|
|
153
|
+
let mutated = original
|
|
154
|
+
for (const [find, replacement] of edits) {
|
|
155
|
+
if (mutated.split(find).length !== 2) throw new Error(`Mutation ${name} does not match exactly once in ${file}: ${find.slice(0, 80)}`)
|
|
156
|
+
mutated = mutated.replace(find, () => replacement)
|
|
157
|
+
}
|
|
158
|
+
if (mutated === original) throw new Error(`Mutation ${name} did not change ${file}`)
|
|
159
|
+
writeFileSync(target, mutated)
|
|
160
|
+
let result
|
|
161
|
+
try { result = run(tests) } finally { writeFileSync(target, original) }
|
|
162
|
+
if (readFileSync(target, 'utf8') !== original) throw new Error(`Restore failed for ${file} after ${name}`)
|
|
163
|
+
const output = result.stdout + '\n' + result.stderr
|
|
164
|
+
const killers = [...new Set([...output.matchAll(/FAIL\s+(server\/\S+\.test\.ts > [^\n]+)/g)].map(x => x[1].trim()))]
|
|
165
|
+
if (result.status === 0 || result.error || !killers.length) { console.log(`${name} SURVIVED or harness failed\n${output.slice(-3000)}`); process.exitCode = 1; continue }
|
|
166
|
+
killed += 1
|
|
167
|
+
console.log(`${name} KILLED by ${killers.slice(0, 2).join(' | ')}${killers.length > 2 ? ` (+${killers.length - 2} more)` : ''}`)
|
|
168
|
+
}
|
|
169
|
+
console.log(`${killed} of ${selected.length} live cues and Ask 6.76.2 mutations killed`)
|
|
170
|
+
} finally { rmSync(scratch, { recursive: true, force: true }) }
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Transcript cleanup 6.77.0 mutation gate: span selection, the deterministic fixes, the edit guards, the ledger, the stop
|
|
3
|
+
// conditions, the screen-context store and route, the dictionary suggestions and the save wiring. Mutations run only in a
|
|
4
|
+
// disposable copy of the checkout; the live checkout is never rewritten.
|
|
5
|
+
//
|
|
6
|
+
// What it proves, in order:
|
|
7
|
+
// 0. The target suites are GREEN unmutated. A gate over a red suite reads every mutant as killed and proves nothing.
|
|
8
|
+
// 1. Each target string appears EXACTLY ONCE in its file, so the mutation is unambiguous.
|
|
9
|
+
// 2. The mutated file differs from the original, so the edit landed.
|
|
10
|
+
// 3. A NAMED test fails ("FAIL server/x.test.ts > name"); a build error or a harness failure is not a kill.
|
|
11
|
+
// 4. The file is restored byte for byte (compared) before the next mutant.
|
|
12
|
+
// Usage: node server/scripts/transcript-cleanup-mutation-gate.mjs [--list | --check] [name…]
|
|
13
|
+
import { mkdtempSync, cpSync, readFileSync, writeFileSync, symlinkSync, rmSync } from 'node:fs'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { resolve, join, dirname, basename } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { spawnSync } from 'node:child_process'
|
|
18
|
+
|
|
19
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
20
|
+
const LIB = ['server/lib/transcript-cleanup.test.ts', 'server/lib/transcript-cleanup-spans.test.ts']
|
|
21
|
+
const ROUTE = ['server/routes/transcript-cleanup.test.ts']
|
|
22
|
+
const MEETING = ['server/routes/meeting.test.ts']
|
|
23
|
+
const WIRE = ['server/routes/health.test.ts', 'server/routes/transcribe.test.ts', 'server/lib/transcript-cleanup-provider.test.ts']
|
|
24
|
+
const ALL = [...LIB, ...ROUTE, ...MEETING, ...WIRE]
|
|
25
|
+
const RUN = 'server/lib/transcript-cleanup.ts', SPANS = 'server/lib/transcript-cleanup-spans.ts', TEXT = 'server/lib/transcript-cleanup-text.ts'
|
|
26
|
+
const MODEL = 'server/lib/transcript-cleanup-model.ts', LEDGER = 'server/lib/transcript-cleanup-ledger.ts', STATE = 'server/lib/transcript-cleanup-state.ts'
|
|
27
|
+
const SCREEN = 'server/lib/transcript-cleanup-screen.ts', SUG = 'server/lib/transcript-cleanup-suggestions.ts', CTX = 'server/lib/transcript-cleanup-context.ts'
|
|
28
|
+
const R = 'server/routes/transcript-cleanup.ts', MEET = 'server/routes/meeting.ts'
|
|
29
|
+
const m = (name, file, edits, tests = LIB) => ({ name, file, edits: typeof edits[0] === 'string' ? [edits] : edits, tests })
|
|
30
|
+
|
|
31
|
+
const mutations = [
|
|
32
|
+
// ── Switch, model, stops ──
|
|
33
|
+
m('switch-checked-first', RUN, ['if (!resolveCleanupSwitch(deps.env, deps.settings).enabled) return', 'if (false) return']),
|
|
34
|
+
m('env-0-forces-off', STATE, [" if (env.COS_TRANSCRIPT_CLEANUP === '0') return { enabled: false, source: 'env' }\n", '']),
|
|
35
|
+
m('env-1-forces-on', STATE, [" if (env.COS_TRANSCRIPT_CLEANUP === '1') return { enabled: true, source: 'env' }\n", '']),
|
|
36
|
+
m('default-off', STATE, [" return { enabled: false, source: 'default' }", " return { enabled: true, source: 'default' }"], [...LIB, ...ROUTE]),
|
|
37
|
+
m('default-model-haiku', STATE, ["return { choice: 'haiku', id: CLEANUP_MODEL_IDS.haiku, source: 'default' }", "return { choice: 'sonnet', id: CLEANUP_MODEL_IDS.sonnet, source: 'default' }"], [...LIB, ...ROUTE]),
|
|
38
|
+
m('setting-model-read', STATE, [" if (stored) return { choice: stored, id: CLEANUP_MODEL_IDS[stored], source: 'setting' }\n", '']),
|
|
39
|
+
m('env-model-wins', STATE, [" if (raw === 'haiku' || raw === 'sonnet') return { choice: raw, id: CLEANUP_MODEL_IDS[raw], source: 'env' }\n", '']),
|
|
40
|
+
m('charge-before-call', RUN, ["else if (!deps.budget.charge(limits)) modelSkipped = 'daily_calls'", "else if (false) modelSkipped = 'daily_calls'"]),
|
|
41
|
+
m('gate-breaker', STATE, [" if (s.breakerOpenUntil !== null && this.now().getTime() < s.breakerOpenUntil) return 'breaker_open'\n", '']),
|
|
42
|
+
m('gate-daily-calls', STATE, [" if (s.calls >= limits.dailyCalls) return 'daily_calls'\n", '']),
|
|
43
|
+
m('gate-daily-tokens', STATE, [" if (s.tokens >= limits.dailyTokens) return 'daily_tokens'\n", '']),
|
|
44
|
+
m('usage-recorded', RUN, [' deps.budget.recordUsage(trace.inputTokens + trace.outputTokens)\n', '']),
|
|
45
|
+
m('provider-limit-opens-breaker', STATE, ['const open = providerLimit || failures >= limits.breakerFailures', 'const open = failures >= limits.breakerFailures']),
|
|
46
|
+
m('breaker-after-three', STATE, [' breakerFailures: 3,', ' breakerFailures: 4,']),
|
|
47
|
+
m('success-closes-breaker', STATE, [' this.write({ ...s, consecutiveFailures: 0, breakerOpenUntil: null })', ' return']),
|
|
48
|
+
m('unreadable-state-is-spent', STATE, [' } catch {\n return spent\n', ' } catch {\n return { day, calls: 0, tokens: 0, consecutiveFailures: 0, breakerOpenUntil: null }\n']),
|
|
49
|
+
m('future-day-is-spent', STATE, [' if (raw.day > day) return { ...spent, ...breaker }\n', '']),
|
|
50
|
+
m('negative-count-is-spent', STATE, [' if (!(calls >= 0) || !(tokens >= 0)) return { ...spent, ...breaker }\n', '']),
|
|
51
|
+
m('failure-recorded', RUN, [' deps.budget.recordFailure(limits, limit)\n', '']),
|
|
52
|
+
m('api-key-guard', RUN, ["else if (deps.env.ANTHROPIC_API_KEY && deps.env.COS_ALLOW_API_FALLBACK !== '1') modelSkipped = 'api_key_in_env'", "else if (false) modelSkipped = 'api_key_in_env'"]),
|
|
53
|
+
m('claude-cli-guard', RUN, ["else if (!deps.claudeAvailable()) modelSkipped = 'claude_not_found'", "else if (false) modelSkipped = 'claude_not_found'"]),
|
|
54
|
+
m('live-session-skipped', RUN, [" if (live) return finish({ status: 'skipped', reason: 'session_live', applied: 0 })", " if (false) return finish({ status: 'skipped', reason: 'session_live', applied: 0 })"]),
|
|
55
|
+
m('span-cap-per-meeting', RUN, ['{ maxSpans: limits.maxSpans, fixed:', '{ maxSpans: 80, fixed:']),
|
|
56
|
+
m('idempotent-rerun', RUN, [" if (alreadyCleaned(input.sessionId, inputSha)) return finish({ status: 'skipped', reason: 'already_done', applied: 0 })", " if (false) return finish({ status: 'skipped', reason: 'already_done', applied: 0 })"]),
|
|
57
|
+
m('ledger-intent-first', RUN, [" if (!appendLedger(input.sessionId, [{ kind: 'run', runId, phase: 'intent', at: at(), inputSha }])) {", " if (!appendLedger(input.sessionId, [{ kind: 'run', runId, phase: 'intent', at: at(), inputSha }]) && false) {"]),
|
|
58
|
+
m('prompt-size-cap', MODEL, [' if (prompt.length + block.length + 1 > MODEL_LIMITS.promptChars) break\n', '']),
|
|
59
|
+
m('screen-lines-in-prompt', MODEL, ['const screen = screenLinesFor(pack.screen, span.atMs, span.near, { max: MODEL_LIMITS.screenLinesPerSpan })', 'const screen: typeof pack.screen = []']),
|
|
60
|
+
m('owner-in-prompt', MODEL, [' if (pack.owner) {\n head.push', ' if (false) {\n head.push']),
|
|
61
|
+
m('parse-scans-past-first-object', MODEL, [" i = text.indexOf('{', i + 1)", ' i = -1']),
|
|
62
|
+
m('parse-string-aware', MODEL, [" else if (c === '\"') inString = false", '']),
|
|
63
|
+
|
|
64
|
+
m('notes-exclude-this-meeting', RUN, ['if (!hit.recordId || hit.sessionId === sessionId) continue', 'if (!hit.recordId) continue']),
|
|
65
|
+
m('notes-bridge-when-memory-empty', RUN, [' if (!memoryHits && !signal.aborted) {', ' if (!signal.aborted) {']),
|
|
66
|
+
m('notes-in-prompt', MODEL, [' for (const n of pack.notes) head.push', ' for (const n of [] as typeof pack.notes) head.push']),
|
|
67
|
+
m('notes-snippet-bounded', CTX, [
|
|
68
|
+
["snippet: redactSecrets(n.snippet).replace(/\\s+/g, ' ').slice(0, CONTEXT_LIMITS.noteChars) }\n pack.notes.push(note)", "snippet: n.snippet }\n pack.notes.push(note)"]]),
|
|
69
|
+
m('notes-snippet-clipped-in-prompt', MODEL, ['${quote(n.title)}: ${clip(n.snippet, 280)}`)', '${quote(n.title)}: ${n.snippet}`)']),
|
|
70
|
+
|
|
71
|
+
// ── Span selection ──
|
|
72
|
+
m('parakeet-floor', SPANS, [" if (engine.startsWith('parakeet')) return LOW_CONFIDENCE.parakeet\n", '']),
|
|
73
|
+
m('fillers-never-doubtful', SPANS, [' && !FILLERS.has(t.key)) flag(t.i, ', ') flag(t.i, ']),
|
|
74
|
+
m('near-name-needs-unsure', SPANS, ['const unsure = input.probability === undefined || input.probability < NEAR_NAME_UNSURE', 'const unsure = true']),
|
|
75
|
+
m('near-name-inflection-skip', SPANS, [' if (inflectionOf(key, t.key)) continue\n', '']),
|
|
76
|
+
m('exact-join-is-a-hit', SPANS, [' if (d === 0 && !input.join) return null', ' if (d === 0) return null']),
|
|
77
|
+
m('join-no-function-words', SPANS, ['group.some(g => g.key.length > 5 || FUNCTION_WORDS.has(g.key))', 'group.some(g => g.key.length > 5)']),
|
|
78
|
+
m('join-pieces-short', SPANS, ['group.some(g => g.key.length > 5 || FUNCTION_WORDS.has(g.key))', 'group.some(g => FUNCTION_WORDS.has(g.key))']),
|
|
79
|
+
m('sounds-alike-onset', TEXT, [' if (vowelOnset(fa) !== vowelOnset(fb)) return false\n', '']),
|
|
80
|
+
m('sounds-alike-two-consonants', TEXT, [' if (sa.length < 2 || sa !== sb) return false', ' if (sa !== sb) return false']),
|
|
81
|
+
m('oov-not-at-sentence-start', SPANS, ['if (fixed.has(t.i) || t.sentenceStart || !/^\\p{Lu}/u.test(t.core)', 'if (fixed.has(t.i) || !/^\\p{Lu}/u.test(t.core)']),
|
|
82
|
+
m('repeat-needs-three', SPANS, ['if (reps >= (unit === 1 ? 4 : 3)) found', 'if (reps >= 2) found']),
|
|
83
|
+
m('merge-stops-at-clause', SPANS, [' if (tokens.slice(to - 1, next).some(t => /[,.;:!?]["\')\\]]*$/.test(t.raw))) break\n', '']),
|
|
84
|
+
m('merge-stays-in-line', SPANS, [' if (tokens[next].line !== tokens[from].line || next - to > 1', ' if (next - to > 1']),
|
|
85
|
+
m('span-word-cap', SPANS, [' maxSpanWords: 6,', ' maxSpanWords: 8,']),
|
|
86
|
+
m('names-ranked-first', SPANS, ["(d.reasons.has('near_name') ? 4 : 0)", "(d.reasons.has('near_name') ? 0 : 0)"]),
|
|
87
|
+
m('align-by-folded-word', TEXT, ['if (words[k].word === token.raw || fold(words[k].word) === token.key)', 'if (words[k].word === token.raw)']),
|
|
88
|
+
m('word-time-on-meeting-clock', TEXT, ['Math.round(startTime + startElapsed + s * 1000)', 'Math.round(startTime + s * 1000)']),
|
|
89
|
+
|
|
90
|
+
// ── Deterministic fixes ──
|
|
91
|
+
m('fix-capitalised-only', SPANS, ['if (!/^\\p{Lu}/u.test(base) || allLower.has(base.toLowerCase()) || known.has(fold(base))) continue', 'if (allLower.has(base.toLowerCase()) || known.has(fold(base))) continue']),
|
|
92
|
+
m('fix-never-a-known-spelling', SPANS, ['if (!/^\\p{Lu}/u.test(base) || allLower.has(base.toLowerCase()) || known.has(fold(base))) continue', 'if (!/^\\p{Lu}/u.test(base) || allLower.has(base.toLowerCase())) continue']),
|
|
93
|
+
m('fix-strong-only-when-unsure', SPANS, ['let replacement = unsure ? fuzzyReplacementFor(base, strong, allLower) : null', 'let replacement = fuzzyReplacementFor(base, strong, allLower)']),
|
|
94
|
+
m('fix-sentence-opener-only-unsure', SPANS, [' if (token.sentenceStart && !unsure) continue\n', '']),
|
|
95
|
+
m('fix-no-inflection', SPANS, ['if (replacement === null || replacement === base || inflectionOf(base, replacement)) continue', 'if (replacement === null || replacement === base) continue']),
|
|
96
|
+
m('fix-no-attendee-targets', RUN, ['const strongTargets = [...pack.vocabulary, ...pack.corrections.map(c => c.written), ...(pack.owner ? [pack.owner.name] : [])]', 'const strongTargets = [...pack.vocabulary, ...pack.corrections.map(c => c.written), ...pack.speakers, ...(pack.owner ? [pack.owner.name] : [])]']),
|
|
97
|
+
|
|
98
|
+
// ── Edit guards ──
|
|
99
|
+
m('only-sent-ids', MODEL, ['const byId = new Map(spans.filter(s => sentIds.includes(s.id)).map(s => [s.id, s]))', 'const byId = new Map(spans.map(s => [s.id, s]))']),
|
|
100
|
+
m('duplicate-rejected', MODEL, [" if (seen.has(id)) { reject('duplicate'); continue }\n", '']),
|
|
101
|
+
m('bracket-is-unsafe', MODEL, ["/[\\u0000-\\u001f\\u007f\\[\\]]/.test(edit!.text", "/[\\u0000-\\u001f\\u007f]/.test(edit!.text"]),
|
|
102
|
+
m('no-op-rejected', MODEL, [" if (text === span.text) { reject('no_op'); continue }\n", '']),
|
|
103
|
+
m('word-count-cap', MODEL, ['newWords.length > spanWords.length + 1 ||', 'newWords.length > spanWords.length + 9 ||']),
|
|
104
|
+
m('char-length-cap', MODEL, [' || text.length > span.text.length * 2 + 8) {', ') {']),
|
|
105
|
+
m('repeat-shrinks-to-own-run', MODEL, [" if (!isRun) { reject('longer_than_span'); continue }\n", '']),
|
|
106
|
+
m('outside-span-before', MODEL, ['(beforeLast && firstNew === beforeLast && !spanKeys.has(beforeLast)) || ', '']),
|
|
107
|
+
m('outside-span-after', MODEL, [' || (afterFirst && lastNew === afterFirst && !spanKeys.has(afterFirst))', '']),
|
|
108
|
+
m('numbers-unchanged', MODEL, [" if (numbers(text) !== numbers(span.text)) { reject('number_unit'); continue }\n", '']),
|
|
109
|
+
m('negations-unchanged', MODEL, [" if (negationCount(text) !== negationCount(span.text)) { reject('negation'); continue }\n", '']),
|
|
110
|
+
m('added-content-rejected', MODEL, [" if (unsupported.length) { reject('added_content'); continue }\n", '']),
|
|
111
|
+
m('context-terms-admit', MODEL, ['const knownTerm = (w: string) => pack.terms.has(fold(w))', 'const knownTerm = (_w: string) => false']),
|
|
112
|
+
|
|
113
|
+
// ── Ledger and writes ──
|
|
114
|
+
m('overlap-dropped', LEDGER, [' if (e.start < cursor || e.end > source.length || e.start >= e.end) continue', ' if (e.end > source.length || e.start >= e.end) continue']),
|
|
115
|
+
m('commit-refuses-changed-files', LEDGER, [" if (markdownNow !== options.markdown || sidecarNow !== options.sidecarText) throw new CleanupWriteError('changed_during_cleanup')\n", '']),
|
|
116
|
+
m('failed-markdown-restores-sidecar', LEDGER, [' try { write(options.sidecarPath, options.sidecarText) } catch { /* the two files now disagree; say so */ }\n', '']),
|
|
117
|
+
m('batch-transcript-kept-canonical', LEDGER, ["sidecar.batchTranscript.replace(/\\n+$/, '') === oldSection) sidecar.batchTranscript = options.transcript", "sidecar.batchTranscript.replace(/\\n+$/, '') === oldSection) sidecar.batchTranscript = sidecar.batchTranscript"]),
|
|
118
|
+
m('lifecycle-revision-bumped', LEDGER, [' sidecar.lifecycleRevision = Number(sidecar.lifecycleRevision ?? 0) + 1\n', '']),
|
|
119
|
+
m('undo-refuses-stale', LEDGER, [" if (at < 0) return { ok: false, status: 409, error: 'stale' }\n", '']),
|
|
120
|
+
m('undo-exact-offsets', LEDGER, [' if (untouched.get(line.index) && line.text.slice(edit.offset, edit.offset + edit.replacement.length) === edit.replacement) at = edit.offset', ' if (false) at = edit.offset']),
|
|
121
|
+
m('undo-records-revert', LEDGER, [" appendLedger(options.sessionId, targets.map(t => ({ kind: 'revert' as const, editId: t.id, at: now })))\n", '']),
|
|
122
|
+
|
|
123
|
+
// ── Screen context ──
|
|
124
|
+
m('screen-secrets-held', SCREEN, [' if (imageTextHasSecrets(o.text) || imageTextHasSecrets(text)) { result.held++; continue }\n', ''], [...LIB, ...ROUTE]),
|
|
125
|
+
m('screen-line-truncated', SCREEN, [' if (text.length > SCREEN_LIMITS.charsPerLine) { text = text.slice(0, SCREEN_LIMITS.charsPerLine); result.truncated++ }\n', '']),
|
|
126
|
+
m('screen-duplicates', SCREEN, [" if (recent.some(r => norm(r.text) === key && Math.abs(r.at - at) <= SCREEN_LIMITS.duplicateWindowMs)) { result.duplicates++; continue }\n", '']),
|
|
127
|
+
m('screen-clock-bounds', SCREEN, ['at === null || at < earliest || at > latest ||', 'at === null ||']),
|
|
128
|
+
m('screen-meeting-line-cap', SCREEN, ['if (result.stored >= SCREEN_LIMITS.linesPerMeeting || storedBytes', 'if (storedBytes']),
|
|
129
|
+
m('screen-request-line-cap', SCREEN, [" if (lines.length > SCREEN_LIMITS.linesPerRequest) throw new ScreenContextError('too_many_lines', 413)\n", ''], [...LIB, ...ROUTE]),
|
|
130
|
+
m('screen-request-byte-cap', SCREEN, [" if (requestBytes > SCREEN_LIMITS.bytesPerRequest) throw new ScreenContextError('request_too_large', 413)\n", ''], [...LIB, ...ROUTE]),
|
|
131
|
+
m('screen-path-confined', SCREEN, [
|
|
132
|
+
[' if (!SESSION_ID.test(sessionId)) return null\n', ''],
|
|
133
|
+
[" return resolve(path).startsWith(resolve(dir) + '/') ? path : null", ' return path']]),
|
|
134
|
+
m('route-unknown-session-404', R, [" if (!meeting) return { ok: false, status: 404, error: 'unknown_session' }\n", ''], ROUTE),
|
|
135
|
+
m('route-cleanup-done-409', R, [" return done ? { ok: false, status: 409, error: 'cleanup_done' } : { ok: true }", ' return { ok: true }'], ROUTE),
|
|
136
|
+
m('route-env-forced-409', R, [" if (hasEnabled && (env.COS_TRANSCRIPT_CLEANUP === '1' || env.COS_TRANSCRIPT_CLEANUP === '0') && body.enabled !== (env.COS_TRANSCRIPT_CLEANUP === '1')) {", ' if (false) {'], ROUTE),
|
|
137
|
+
m('sweep-keeps-known', SCREEN, [' try { gone = !opts.known(sessionId) } catch { gone = false }', ' gone = true'], ROUTE),
|
|
138
|
+
m('sweep-screen-retention', SCREEN, [" const expired = kind === 'screen' && age > retentionMs", ' const expired = false'], ROUTE),
|
|
139
|
+
|
|
140
|
+
// ── Dictionary suggestions ──
|
|
141
|
+
m('suggestion-needs-evidence', SUG, ['if (!written || written.length > 100 || !input.evidence.length) return null', 'if (!written || written.length > 100) return null'], [...LIB, ...ROUTE]),
|
|
142
|
+
m('suggestion-not-for-dictionary-words', SUG, [' if (input.inDictionary(written)) return null\n', ''], ROUTE),
|
|
143
|
+
m('suggestion-dismissed-stays-out', SUG, [" if (existing && existing.status !== 'pending') return null\n", ''], ROUTE),
|
|
144
|
+
m('suggestion-dedupe-case', SUG, ['const existing = list.find(s => same(s.written, written))', 'const existing = list.find(s => s.written === written)'], ROUTE),
|
|
145
|
+
m('orchestrator-skips-dictionary-terms', RUN, [
|
|
146
|
+
[' if (!term || inDictionary(term) || heardKeys.has(term.key)) continue', ' if (!term || heardKeys.has(term.key)) continue'],
|
|
147
|
+
[" if (deps.suggestions.record({ written: term.display, heard: edit.original, evidence, sessionId, at, inDictionary: w => dictionary.has(w.trim().toLowerCase()) })) recorded++", " if (deps.suggestions.record({ written: term.display, heard: edit.original, evidence, sessionId, at, inDictionary: () => false })) recorded++"]]),
|
|
148
|
+
m('orchestrator-longest-term', RUN, [' if (parts.every(p => covered.has(p))) continue\n', '']),
|
|
149
|
+
m('evidence-maps-screen', CTX, ["['screen', 'screen'], ", '']),
|
|
150
|
+
m('accept-uses-vocabulary-writer', R, [' : await addVocabulary(suggestion.written)', " : { status: 200, body: { unchanged: false, glossary: null } }"], ROUTE),
|
|
151
|
+
m('accept-correction-path', R, ["const outcome = body.correction === true && heard", 'const outcome = false && heard'], ROUTE),
|
|
152
|
+
m('accept-refuses-dismissed', R, [" if (suggestion.status === 'dismissed') return res.status(409).json({ error: 'suggestion_dismissed' })\n", ''], ROUTE),
|
|
153
|
+
|
|
154
|
+
// ── Save wiring and capability ──
|
|
155
|
+
m('cleanup-stage-in-finalization', MEET, [' await runCleanupStage(runtime.transcriptCleanup, { sessionId: current.sessionId, meetingPath: current.meetingPath, sidecarPath: current.sidecarPath }, runtime.sessions)\n', ''], MEETING),
|
|
156
|
+
m('cleanup-stage-never-fails-save', MEET, [' try {\n await run(input)\n } catch (error) {', ' {\n await run(input)\n } if (false) { const error = null as unknown;'], MEETING),
|
|
157
|
+
m('health-says-off', R, [' return { protocol: TRANSCRIPT_CLEANUP_PROTOCOL, enabled }', ' return { protocol: TRANSCRIPT_CLEANUP_PROTOCOL, enabled: true }'], [...ROUTE, ...WIRE]),
|
|
158
|
+
m('capabilities-advertised', 'server/routes/transcribe.ts', [' transcriptCleanup: { protocol: 1, suggestions: 1, screenContext: 1 },\n', ''], WIRE),
|
|
159
|
+
m('provider-source-tag', RUN, ["source: 'transcript-cleanup', maxOutputTokens: opts.maxOutputTokens", "source: 'lens_gist', maxOutputTokens: opts.maxOutputTokens"], WIRE),
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
const args = process.argv.slice(2)
|
|
163
|
+
if (args.includes('--list')) { for (const { name } of mutations) console.log(name); process.exit(0) }
|
|
164
|
+
const selected = args.length && !args.includes('--check') ? mutations.filter(({ name }) => args.includes(name)) : mutations
|
|
165
|
+
if (args.length && !args.includes('--check') && selected.length !== args.length) throw new Error('Unknown mutation name in: ' + args.join(' '))
|
|
166
|
+
if (new Set(mutations.map(x => x.name)).size !== mutations.length) throw new Error('Duplicate mutation name')
|
|
167
|
+
// --check: every target matches exactly once and changes its file. Runs no tests.
|
|
168
|
+
if (args.includes('--check')) {
|
|
169
|
+
let bad = 0
|
|
170
|
+
for (const { name, file, edits } of mutations) {
|
|
171
|
+
let text = readFileSync(join(root, file), 'utf8')
|
|
172
|
+
const original = text
|
|
173
|
+
for (const [find, replacement] of edits) {
|
|
174
|
+
const n = text.split(find).length - 1
|
|
175
|
+
if (n !== 1) { console.log(`${name}: matches ${n} times in ${file}: ${find.slice(0, 90)}`); bad++ }
|
|
176
|
+
else text = text.replace(find, () => replacement)
|
|
177
|
+
}
|
|
178
|
+
if (text === original) { console.log(`${name}: no change`); bad++ }
|
|
179
|
+
}
|
|
180
|
+
console.log(`${mutations.length - bad} of ${mutations.length} targets match exactly once`)
|
|
181
|
+
process.exit(bad ? 1 : 0)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const scratch = mkdtempSync(join(tmpdir(), 'transcript-cleanup-mutations-'))
|
|
185
|
+
try {
|
|
186
|
+
const skip = new Set(['data', 'models', 'certs', 'node_modules'])
|
|
187
|
+
cpSync(join(root, 'server'), join(scratch, 'server'), { recursive: true, filter: src => !(skip.has(basename(src)) && dirname(src) === join(root, 'server')) })
|
|
188
|
+
for (const dir of ['shared', 'work-task-runtime', 'bin', 'docs']) cpSync(join(root, dir), join(scratch, dir), { recursive: true })
|
|
189
|
+
for (const file of ['vitest.config.ts', 'package.json', 'managed-runtime-contract.json']) cpSync(join(root, file), join(scratch, file))
|
|
190
|
+
symlinkSync(join(root, 'node_modules'), join(scratch, 'node_modules'), 'dir')
|
|
191
|
+
const run = tests => spawnSync(process.execPath, [join(root, 'node_modules/vitest/vitest.mjs'), 'run', '--maxWorkers=1', ...tests], {
|
|
192
|
+
cwd: scratch, env: { PATH: process.env.PATH, HOME: scratch, TMPDIR: process.env.TMPDIR, COS_DATA_DIR: join(scratch, 'data'), COS_PROFILE_PATH: join(scratch, 'data', 'profile.json') },
|
|
193
|
+
encoding: 'utf8', timeout: 300000,
|
|
194
|
+
})
|
|
195
|
+
const baseline = run(ALL)
|
|
196
|
+
if (baseline.status !== 0) throw new Error('Baseline failed; a gate over a red suite proves nothing:\n' + baseline.stdout + '\n' + baseline.stderr)
|
|
197
|
+
const totals = /Tests\s+(\d+) passed/.exec(baseline.stdout)
|
|
198
|
+
console.log(`baseline PASS (${totals ? totals[1] : '?'} tests in ${ALL.length} files)`)
|
|
199
|
+
let killed = 0
|
|
200
|
+
for (const { name, file, edits, tests } of selected) {
|
|
201
|
+
const target = join(scratch, file), original = readFileSync(target, 'utf8')
|
|
202
|
+
let mutated = original
|
|
203
|
+
for (const [find, replacement] of edits) {
|
|
204
|
+
if (mutated.split(find).length !== 2) throw new Error(`Mutation ${name} does not match exactly once in ${file}: ${find.slice(0, 80)}`)
|
|
205
|
+
mutated = mutated.replace(find, () => replacement)
|
|
206
|
+
}
|
|
207
|
+
if (mutated === original) throw new Error(`Mutation ${name} did not change ${file}`)
|
|
208
|
+
writeFileSync(target, mutated)
|
|
209
|
+
let result
|
|
210
|
+
try { result = run(tests) } finally { writeFileSync(target, original) }
|
|
211
|
+
if (readFileSync(target, 'utf8') !== original) throw new Error(`Restore failed for ${file} after ${name}`)
|
|
212
|
+
const output = result.stdout + '\n' + result.stderr
|
|
213
|
+
const killers = [...new Set([...output.matchAll(/FAIL\s+(server\/\S+\.test\.ts > [^\n]+)/g)].map(x => x[1].trim()))]
|
|
214
|
+
if (result.status === 0 || result.error || !killers.length) { console.log(`${name} SURVIVED or harness failed\n${output.slice(-3000)}`); process.exitCode = 1; continue }
|
|
215
|
+
killed += 1
|
|
216
|
+
console.log(`${name} KILLED by ${killers.slice(0, 2).join(' | ')}${killers.length > 2 ? ` (+${killers.length - 2} more)` : ''}`)
|
|
217
|
+
}
|
|
218
|
+
console.log(`${killed} of ${selected.length} transcript cleanup 6.77.0 mutations killed`)
|
|
219
|
+
} finally { rmSync(scratch, { recursive: true, force: true }) }
|
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
|
package/shared/meeting-ask.ts
CHANGED
|
@@ -40,7 +40,8 @@ export interface MeetingAskSource {
|
|
|
40
40
|
/** Server-owned accepted post-stop quotation; no live turn target or precise word clock. */
|
|
41
41
|
finalizedTranscript?: boolean
|
|
42
42
|
id: string;
|
|
43
|
-
|
|
43
|
+
/** 'profile' (server 6.76.0): the COS owner card from the profile; cited with an H id like other COS history. */
|
|
44
|
+
kind: 'transcript' | 'memory' | 'meeting' | 'file' | 'web' | 'profile';
|
|
44
45
|
title: string;
|
|
45
46
|
text: string;
|
|
46
47
|
turnId?: number;
|
|
@@ -115,6 +116,8 @@ export function meetingAskFromLine(sections: readonly MeetingAskSection[] | unde
|
|
|
115
116
|
} }));
|
|
116
117
|
parts.push(hosts.length ? `the web, ${hosts.slice(0, 2).join(', ')}` : 'the web');
|
|
117
118
|
}
|
|
119
|
+
if (used.some(s => s.kind === 'profile'))
|
|
120
|
+
parts.push('COS profile');
|
|
118
121
|
if (used.some(s => s.kind === 'memory'))
|
|
119
122
|
parts.push('COS memory');
|
|
120
123
|
const meetings = unique(used.filter(s => s.kind === 'meeting').map(s => {
|
|
@@ -170,6 +173,8 @@ export interface MeetingAskExchange {
|
|
|
170
173
|
files: MeetingAskFile[];
|
|
171
174
|
error?: string;
|
|
172
175
|
retryOf?: string;
|
|
176
|
+
/** Server 6.76.0: how long the answer took on the server and in which phase. Not the meeting position; that is throughMs. */
|
|
177
|
+
timings?: MeetingAskTimings;
|
|
173
178
|
/** Server 6.71.13 (capabilities.factCheckScope): what a completed Fact check covered. Named-claim checks and follow-ups carry none. */
|
|
174
179
|
factCheckRange?: MeetingAskFactCheckRange;
|
|
175
180
|
}
|
|
@@ -178,6 +183,23 @@ export interface MeetingAskExchange {
|
|
|
178
183
|
* check covers the whole meeting (full); each later one covers only what is new since the last completed
|
|
179
184
|
* check's throughMs. A check that found nothing new still completes with claims 0, so the next one moves on.
|
|
180
185
|
*/
|
|
186
|
+
/**
|
|
187
|
+
* One phase of one answer, in wall-clock ms on the server (6.76.0). plan is a model call that asked for a lookup,
|
|
188
|
+
* answer the one that wrote the draft, review the grounding check, public_topic the fact-check topic selection,
|
|
189
|
+
* fact_check the whole fact-check route. forced marks the COS lookup a person question always gets.
|
|
190
|
+
*/
|
|
191
|
+
export interface MeetingAskPhaseTiming {
|
|
192
|
+
phase: 'plan' | 'answer' | 'review' | 'cos_search' | 'web_search' | 'public_topic' | 'fact_check';
|
|
193
|
+
ms: number;
|
|
194
|
+
forced?: true;
|
|
195
|
+
failed?: true;
|
|
196
|
+
results?: number;
|
|
197
|
+
}
|
|
198
|
+
export interface MeetingAskTimings {
|
|
199
|
+
/** From the server starting the request to its final state. */
|
|
200
|
+
totalMs: number;
|
|
201
|
+
phases: MeetingAskPhaseTiming[];
|
|
202
|
+
}
|
|
181
203
|
export interface MeetingAskFactCheckRange {
|
|
182
204
|
fromMs: number;
|
|
183
205
|
throughMs: number;
|