@gotcos/glasses-server 6.46.1 → 6.47.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 +13 -0
- package/package.json +6 -2
- package/server/index.ts +76 -0
- package/server/lib/cos-operations-meetings.ts +99 -8
- package/server/lib/fireflies-client.ts +862 -0
- package/server/lib/fireflies-key.ts +182 -0
- package/server/lib/imported-library-rows.ts +616 -0
- package/server/lib/imported-meeting-library.ts +608 -0
- package/server/lib/maintenance-lifecycle.ts +14 -0
- package/server/lib/meeting-actions-store.ts +478 -0
- package/server/lib/meeting-actions.ts +2583 -0
- package/server/lib/meeting-corrections.ts +32 -1
- package/server/lib/meeting-decisions.ts +223 -0
- package/server/lib/meeting-engine/align.ts +167 -0
- package/server/lib/meeting-engine/attribute.ts +265 -0
- package/server/lib/meeting-engine/evidence.ts +428 -0
- package/server/lib/meeting-engine/pairing.ts +327 -0
- package/server/lib/meeting-engine/render.ts +694 -0
- package/server/lib/meeting-engine/split.ts +242 -0
- package/server/lib/meeting-engine/worker.ts +238 -0
- package/server/lib/meeting-engine-mode.ts +197 -0
- package/server/lib/meeting-file-guards.ts +141 -0
- package/server/lib/meeting-import.ts +763 -0
- package/server/lib/meeting-library-search.ts +146 -11
- package/server/lib/meeting-parse.ts +184 -0
- package/server/lib/meeting-store.ts +108 -275
- package/server/lib/meeting-suggestion-sides.ts +242 -0
- package/server/lib/morning-brief-runtime.ts +20 -8
- package/server/lib/pipeline-runner.ts +227 -0
- package/server/lib/voice-evidence-guard.ts +87 -0
- package/server/routes/fireflies-key.ts +102 -0
- package/server/routes/meeting-actions.ts +82 -0
- package/server/routes/meeting-engine.ts +52 -0
- package/server/routes/meeting-import.ts +67 -0
- package/server/routes/meeting-suggestions.ts +66 -0
- package/server/routes/meeting.ts +117 -10
- package/server/routes/meetings.ts +177 -50
- package/server/routes/voice.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 6.47.0
|
|
2
|
+
|
|
3
|
+
Meetings from Fireflies join the library, and recordings of the same meeting merge into one richer record. Paired Control build: 0.5.230. Glasses 6.9.474 adds Ref on the lens and needs no server change.
|
|
4
|
+
|
|
5
|
+
- Fireflies import, on Macs without the COS pipeline. Paste a Fireflies API key in COS Control; the server checks it and reports a typed state (ok, invalid key, rate limited, vendor down, unreachable, no key). Imports arrive through the Fireflies API only, never from the desktop app's files, with a 7, 30 or 90 day backfill that resumes across the plan's daily limit, a 6 hour poll that uses at most half of the day's budget, and a ledger that reconciles what the vendor listed against what was written. Every failure has its own state; a run that imported nothing and failed reads as the failure. Imported meetings carry Fireflies' overview and action items.
|
|
6
|
+
- A deterministic merge engine, on a worker thread. It pairs a G2 recording with the Fireflies meeting that shares the most unique three-word phrases at a consistent time offset, refuses a pairing whose clocks disagree by more than 15 minutes, collapses duplicate recordings of one meeting, and groups every G2 recording of a meeting into one merged record. No language model is involved anywhere in import, pairing, alignment, splitting or merging.
|
|
7
|
+
- Merges are additive. A merged record holds the Fireflies transcript, the full G2 capture, any alternate transcript, the union of attendees, the vendor summary and action items, and a Sources table. Where the recordings align, G2 voice matches label generic Fireflies speakers, shown with their score, and the original label is kept. Source files are never modified.
|
|
8
|
+
- Every automatic action can be undone. High confidence merges apply automatically with Undo; an undone merge is never repeated by automation. Lower confidence pairings become suggestions to confirm or dismiss. Recordings made before the engine was installed only ever become suggestions. Content-anchored splits of long recordings apply automatically with Revert on a Mac WITHOUT the COS pipeline, where a split writes new records; on a Mac with the pipeline a split is only ever a suggestion, because there is no additive, revertible way to turn one operations scribe into three, and accepting one in apply mode is refused with `split_not_supported_in_apply_mode`.
|
|
9
|
+
- Macs with the COS pipeline start in advise mode: the engine shows would-merge items and suggestions and writes nothing into the operations tree. Switching to apply in Control hands each decision to sync_meetings.py, which archives the original to a unique verified path, splices the merge into the existing scribe (title, summary, decisions and action items survive), and can revert byte for byte. One mode file is read by every pipeline process; there is no environment flag.
|
|
10
|
+
- Lists, detail, search, day counts and speaker review understand imported and merged records. A merged row replaces the rows it merged; its sources stay reachable from its detail. Imported and merged records are read only for speaker mutations, and never train voice profiles.
|
|
11
|
+
- New routes: fireflies-key set, status, check and delete; meeting-import fireflies run, status and settings; meeting-suggestions list, accept, confirm and dismiss; meeting-actions list, one by id, revert, retry and revert-all; meeting-engine status and mode. A failed merge or undo is not terminal: retry puts it back in its own direction with a fresh attempt budget. Every suggestion carries both sides resolved server-side (title, start, length, source and the record that still holds each one), so a pipeline Mac can read them. The contract is in docs/meeting-merge-contract.md.
|
|
12
|
+
- Compatibility: COS Control 0.5.229 and older show these features as unavailable and keep everything else. Glasses 6.9.473 lists merged meetings under an older source label. Older servers ignore the imports folder entirely. Rollback: run revert-all first if merged records should not remain, then set the mode back to advise before downgrading.
|
|
13
|
+
|
|
1
14
|
## 6.46.1
|
|
2
15
|
|
|
3
16
|
G2 recordings show in a multi-folder Meetings library that has no COS pipeline to file them. No Control or glasses update is needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.47.0",
|
|
4
4
|
"description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"start": "node bin/cli.cjs",
|
|
13
13
|
"start:server": "node --import tsx/esm server/index.ts",
|
|
14
14
|
"test": "vitest run --maxWorkers=1",
|
|
15
|
-
"typecheck": "tsc --noEmit"
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"mutation:gate": "node server/scripts/mutation-gate.mjs"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"even-g2",
|
|
@@ -36,6 +37,9 @@
|
|
|
36
37
|
"server/models/silero_vad.onnx",
|
|
37
38
|
"shared",
|
|
38
39
|
"!server/**/*.test.ts",
|
|
40
|
+
"!server/scripts/engine-canary.ts",
|
|
41
|
+
"!server/scripts/mutation-gate.mjs",
|
|
42
|
+
"!server/lib/meeting-engine/__fixtures__/**",
|
|
39
43
|
"!shared/**/*.test.ts",
|
|
40
44
|
".env.example",
|
|
41
45
|
".cos-profile.example.json",
|
package/server/index.ts
CHANGED
|
@@ -43,6 +43,20 @@ import { meetingRouter, resumeMeetingFinalizationJobs } from './routes/meeting.j
|
|
|
43
43
|
import { meetingsRouter } from './routes/meetings.js'
|
|
44
44
|
import { openaiCompatRouter } from './routes/openai-compat.js'
|
|
45
45
|
import { openaiKeyRouter } from './routes/openai-key.js'
|
|
46
|
+
import { firefliesKeyRouter } from './routes/fireflies-key.js'
|
|
47
|
+
import { meetingImportRouter } from './routes/meeting-import.js'
|
|
48
|
+
import { startMeetingImportScheduler, stopMeetingImportScheduler } from './lib/meeting-import.js'
|
|
49
|
+
import { meetingSuggestionsRouter } from './routes/meeting-suggestions.js'
|
|
50
|
+
import { meetingActionsRouter } from './routes/meeting-actions.js'
|
|
51
|
+
import { meetingEngineRouter } from './routes/meeting-engine.js'
|
|
52
|
+
import {
|
|
53
|
+
redrivePendingMergeActions,
|
|
54
|
+
sweepOrphanDecisions,
|
|
55
|
+
startMeetingMergeScheduler,
|
|
56
|
+
stopMeetingMergeScheduler,
|
|
57
|
+
} from './lib/meeting-actions.js'
|
|
58
|
+
import { getImportedMeetingLibrary } from './lib/imported-meeting-library.js'
|
|
59
|
+
import { maintenanceLifecycle } from './lib/maintenance-lifecycle.js'
|
|
46
60
|
import { messageRefRouter } from './routes/message-ref.js'
|
|
47
61
|
import { archiveRouter } from './routes/archive.js'
|
|
48
62
|
import { sessionsRouter } from './routes/sessions.js'
|
|
@@ -220,6 +234,18 @@ app.use('/api', (req, res, next) => {
|
|
|
220
234
|
|| req.path === '/transcribe'
|
|
221
235
|
|| req.path.startsWith('/transcribe-stream')
|
|
222
236
|
|| req.path === '/meeting/save'
|
|
237
|
+
// The import run answers 202 and keeps working. It takes its own
|
|
238
|
+
// maintenance lease per page and checks admissions itself, so the global
|
|
239
|
+
// request lease would only double-own the same work.
|
|
240
|
+
|| req.path === '/meeting-import/fireflies/run'
|
|
241
|
+
// The suggestion and action mutations queue onto the merge runner, which takes its
|
|
242
|
+
// own `meeting_merge` lease PER ACTION and catches a drain by deferring that action.
|
|
243
|
+
// The global request lease would double-own the same work, and in apply mode it
|
|
244
|
+
// would hold across a 75s pipeline spawn — past COS Control's 90s drain timeout.
|
|
245
|
+
// Each of these still refuses on its own when admissions are closed.
|
|
246
|
+
|| req.path.startsWith('/meeting-suggestions/')
|
|
247
|
+
|| req.path.startsWith('/meeting-actions/')
|
|
248
|
+
|| req.path === '/meeting-engine/mode'
|
|
223
249
|
|| req.path.startsWith('/prompt-drafts')
|
|
224
250
|
|| req.path.startsWith('/maintenance/drain')
|
|
225
251
|
if (lifecycleOwned) return next()
|
|
@@ -675,6 +701,13 @@ app.use('/api', memoryRouter)
|
|
|
675
701
|
app.use('/api', threadsRouter)
|
|
676
702
|
app.use('/api', skillsRouter)
|
|
677
703
|
app.use('/api', openaiKeyRouter)
|
|
704
|
+
// Fireflies: the key, and importing meetings this Mac did not record (6.47.0).
|
|
705
|
+
app.use('/api', firefliesKeyRouter)
|
|
706
|
+
app.use('/api', meetingImportRouter)
|
|
707
|
+
// The merge engine: what it wants decided, what it did, and its own state (6.47.0).
|
|
708
|
+
app.use('/api', meetingSuggestionsRouter)
|
|
709
|
+
app.use('/api', meetingActionsRouter)
|
|
710
|
+
app.use('/api', meetingEngineRouter)
|
|
678
711
|
// v6.3.0 — Message History, cross-day 'reference message N', and history
|
|
679
712
|
// recovery for public npx users (previously full-COS-server only).
|
|
680
713
|
app.use('/api', messageRefRouter)
|
|
@@ -725,6 +758,8 @@ async function gracefulShutdown(): Promise<void> {
|
|
|
725
758
|
const forceExit = setTimeout(() => process.exit(1), 8_000)
|
|
726
759
|
forceExit.unref?.()
|
|
727
760
|
stopMorningBriefScheduler()
|
|
761
|
+
stopMeetingImportScheduler()
|
|
762
|
+
stopMeetingMergeScheduler()
|
|
728
763
|
try {
|
|
729
764
|
await shutdownQueryJobRuntime('server_shutdown')
|
|
730
765
|
} catch (error) {
|
|
@@ -915,6 +950,47 @@ listenRequiredServers(listeners).then(() => {
|
|
|
915
950
|
// was interrupted by a prior server update or process exit.
|
|
916
951
|
resumeMeetingFinalizationJobs()
|
|
917
952
|
|
|
953
|
+
// Debris from an import killed mid-write: a temp file, or a sidecar whose
|
|
954
|
+
// markdown never landed. Both are invisible to every reader, so nothing
|
|
955
|
+
// would ever surface them. Skipped while a writer holds the lease, because
|
|
956
|
+
// mid-write that sidecar is the correct intermediate state.
|
|
957
|
+
try {
|
|
958
|
+
const swept = getImportedMeetingLibrary().sweep({
|
|
959
|
+
isBusy: () => (maintenanceLifecycle.snapshot().activeByKind.meeting_import ?? 0) > 0,
|
|
960
|
+
})
|
|
961
|
+
if (swept.removedTemp > 0 || swept.removedSidecars > 0) {
|
|
962
|
+
console.log(`[imports] startup sweep: ${swept.removedTemp} temp file(s), ${swept.removedSidecars} orphan sidecar(s)`)
|
|
963
|
+
}
|
|
964
|
+
} catch (error) {
|
|
965
|
+
console.warn('[imports] startup sweep failed', error)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// The Fireflies poll. Its own tick refuses in advise mode, without a key,
|
|
969
|
+
// while admissions are closed, and past its share of the daily budget, so
|
|
970
|
+
// starting it here is safe in every configuration.
|
|
971
|
+
startMeetingImportScheduler()
|
|
972
|
+
|
|
973
|
+
// The merge engine's own 30s tick (a full pass every six hours), and the
|
|
974
|
+
// central speaker-correction hook that re-derives affected records. Both
|
|
975
|
+
// defer while a capture is live or admissions are closed.
|
|
976
|
+
startMeetingMergeScheduler()
|
|
977
|
+
|
|
978
|
+
// AFTER the imports sweep, not before: the sweep removes a killed write's
|
|
979
|
+
// debris, and re-driving a pending apply into a half-written decision is
|
|
980
|
+
// exactly what the sweep exists to prevent. A pending action is one a
|
|
981
|
+
// previous process wrote and did not finish; the pipeline steps are
|
|
982
|
+
// idempotent, so re-driving one that actually completed is a no-op.
|
|
983
|
+
void redrivePendingMergeActions()
|
|
984
|
+
.then(count => { if (count > 0) console.log(`[meeting-merge] re-drove ${count} waiting action(s)`) })
|
|
985
|
+
.then(() => {
|
|
986
|
+
// A decision file is written BEFORE its action row, so a crash in that window
|
|
987
|
+
// leaves transcript text nothing will ever read or delete. Runs after the
|
|
988
|
+
// re-drive, so an action being driven right now still owns its decision.
|
|
989
|
+
const removed = sweepOrphanDecisions()
|
|
990
|
+
if (removed > 0) console.log(`[meeting-merge] removed ${removed} orphan decision file(s)`)
|
|
991
|
+
})
|
|
992
|
+
.catch(error => console.warn('[meeting-merge] pending re-drive failed', error))
|
|
993
|
+
|
|
918
994
|
void initQueryJobRuntime().then(health => {
|
|
919
995
|
if (process.env.COS_DURABLE_QUERY_JOBS !== '0') {
|
|
920
996
|
console.log(`[COS API] Durable query jobs: ${health.store.state} · ${health.store.retainedIdentities} retained`)
|
|
@@ -182,6 +182,37 @@ export function sidecarSessionId(monthDir: string, meetingFilename: string): str
|
|
|
182
182
|
return sidecarListHints(monthDir, meetingFilename).sessionId
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// ── Merged-scribe markers (6.47.0, apply mode) ───────────────────────────────
|
|
186
|
+
//
|
|
187
|
+
// On a Mac with the COS pipeline the merge is spliced INTO the existing Fireflies
|
|
188
|
+
// scribe rather than written as a new record, so there is no `.derived.json` to
|
|
189
|
+
// read and the scribe itself has to say what it holds. The pipeline writes four
|
|
190
|
+
// markers (WS8 step 4); these two are the ones a row identity needs.
|
|
191
|
+
//
|
|
192
|
+
// WHY THE SCRIBE AND NOT A SIDECAR. A merged scribe replaced a Fireflies scribe,
|
|
193
|
+
// which never had a `.g2-chunks.json`. Its G2 sidecar stays beside the retired
|
|
194
|
+
// standalone capture, so the only place that says "this file holds session X" is
|
|
195
|
+
// the file.
|
|
196
|
+
|
|
197
|
+
/** `<!-- g2-session: <sessionId> -->`, once per G2 capture the scribe holds. */
|
|
198
|
+
export const G2_SESSION_MARKER = /<!--\s*g2-session:\s*([A-Za-z0-9:_-]{3,96})\s*-->/g
|
|
199
|
+
/** `<!-- merge-action: <actionId> -->`, so Control can offer the merge's Undo. */
|
|
200
|
+
export const MERGE_ACTION_MARKER = /<!--\s*merge-action:\s*([A-Za-z0-9_-]{1,64})\s*-->/
|
|
201
|
+
|
|
202
|
+
/** Every session a merged scribe declares, in the order it declares them. */
|
|
203
|
+
export function mergedScribeSessions(content: string): string[] {
|
|
204
|
+
const sessions: string[] = []
|
|
205
|
+
for (const match of content.matchAll(G2_SESSION_MARKER)) {
|
|
206
|
+
if (!sessions.includes(match[1])) sessions.push(match[1])
|
|
207
|
+
}
|
|
208
|
+
return sessions
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The action a merged scribe was written by, when it declares one. */
|
|
212
|
+
export function mergedScribeActionId(content: string): string | undefined {
|
|
213
|
+
return content.match(MERGE_ACTION_MARKER)?.[1]
|
|
214
|
+
}
|
|
215
|
+
|
|
185
216
|
function envPath(name: string): string | null {
|
|
186
217
|
const raw = process.env[name]?.trim()
|
|
187
218
|
if (!raw) return null
|
|
@@ -439,6 +470,14 @@ function withMeetingListInsights(meta: CosOperationsMeetingMeta, content: string
|
|
|
439
470
|
*
|
|
440
471
|
* Scans sidecar heads rather than parsing them, so the cost is a 4 KB read per
|
|
441
472
|
* candidate and not the transcript.
|
|
473
|
+
*
|
|
474
|
+
* A MERGED SCRIBE OUTRANKS AN ORPHAN SIDECAR (6.47.0). Applying a merge splices
|
|
475
|
+
* the capture into the Fireflies scribe and then RETIRES the G2 standalone
|
|
476
|
+
* scribe, leaving its `.g2-chunks.json` behind with no markdown beside it. The
|
|
477
|
+
* chunks are still the evidence — that sidecar is what the speaker review reads —
|
|
478
|
+
* but the readable meeting is now the merged scribe, so this returns the orphan's
|
|
479
|
+
* sidecar path with the merged scribe's path, filename and title. Without that,
|
|
480
|
+
* naming a voice on a merged meeting opened a file that no longer existed.
|
|
442
481
|
*/
|
|
443
482
|
export function findCosOperationsMeetingBySessionId(sessionId: string): {
|
|
444
483
|
sidecarPath: string
|
|
@@ -468,17 +507,29 @@ export function findCosOperationsMeetingBySessionId(sessionId: string): {
|
|
|
468
507
|
for (const sidecarName of sidecars) {
|
|
469
508
|
const meetingFilename = sidecarName.replace(/\.g2-chunks\.json$/, '.md')
|
|
470
509
|
if (sidecarSessionId(monthDir, meetingFilename) !== sessionId) continue
|
|
471
|
-
const
|
|
472
|
-
let
|
|
510
|
+
const sidecarPath = join(monthDir, sidecarName)
|
|
511
|
+
let meetingPath = join(monthDir, meetingFilename)
|
|
512
|
+
let resolvedFilename = meetingFilename
|
|
513
|
+
let content: string | null = null
|
|
473
514
|
try {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
515
|
+
content = readFileSync(meetingPath, 'utf-8')
|
|
516
|
+
} catch {
|
|
517
|
+
// The capture's own scribe is gone. Only now is it worth reading the
|
|
518
|
+
// month's other scribes to find the one that declares this session.
|
|
519
|
+
const merged = findMergedScribeForSession(monthDir, sessionId)
|
|
520
|
+
if (merged) {
|
|
521
|
+
resolvedFilename = merged.filename
|
|
522
|
+
meetingPath = join(monthDir, merged.filename)
|
|
523
|
+
content = merged.content
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
let title = resolvedFilename.replace(/\.md$/, '')
|
|
527
|
+
const heading = content?.slice(0, 4000).match(/^#\s+(.+)$/m)?.[1]?.trim()
|
|
528
|
+
if (heading) title = heading
|
|
478
529
|
return {
|
|
479
|
-
sidecarPath
|
|
530
|
+
sidecarPath,
|
|
480
531
|
meetingPath,
|
|
481
|
-
filename:
|
|
532
|
+
filename: resolvedFilename,
|
|
482
533
|
domain,
|
|
483
534
|
month,
|
|
484
535
|
title,
|
|
@@ -489,6 +540,32 @@ export function findCosOperationsMeetingBySessionId(sessionId: string): {
|
|
|
489
540
|
return null
|
|
490
541
|
}
|
|
491
542
|
|
|
543
|
+
/**
|
|
544
|
+
* The scribe in this month that declares `sessionId` through `<!-- g2-session -->`.
|
|
545
|
+
*
|
|
546
|
+
* Only ever called when a capture's own scribe is missing, so the full-content
|
|
547
|
+
* read it costs lands on a path that was about to answer with a dead file.
|
|
548
|
+
*/
|
|
549
|
+
function findMergedScribeForSession(
|
|
550
|
+
monthDir: string,
|
|
551
|
+
sessionId: string,
|
|
552
|
+
): { filename: string; content: string } | null {
|
|
553
|
+
let names: string[]
|
|
554
|
+
try {
|
|
555
|
+
names = readdirSync(monthDir).filter(name => name.endsWith('.md')).sort().reverse()
|
|
556
|
+
} catch {
|
|
557
|
+
return null
|
|
558
|
+
}
|
|
559
|
+
for (const filename of names.slice(0, MAX_LIST_CANDIDATES)) {
|
|
560
|
+
let content: string
|
|
561
|
+
try {
|
|
562
|
+
content = readFileSync(join(monthDir, filename), 'utf-8')
|
|
563
|
+
} catch { continue }
|
|
564
|
+
if (mergedScribeSessions(content).includes(sessionId)) return { filename, content }
|
|
565
|
+
}
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
568
|
+
|
|
492
569
|
export type MeetingLibraryRecord = NonNullable<ReturnType<typeof findCosOperationsMeetingBySessionId>> & {
|
|
493
570
|
recordId?: string
|
|
494
571
|
librarySource?: 'direct_library' | 'cos_operations'
|
|
@@ -588,6 +665,20 @@ export function listCosOperationsMeetings(options: {
|
|
|
588
665
|
if (hints.speakers.length > 0) {
|
|
589
666
|
meta.voiceReview = meetingVoiceReview(hints.speakers, hints.sessionId)
|
|
590
667
|
}
|
|
668
|
+
// A scribe the pipeline merged says so in its own body: it holds
|
|
669
|
+
// captures whose standalone rows this list must then drop, and it
|
|
670
|
+
// names the action Control offers Undo for. The content is already
|
|
671
|
+
// in memory, so this costs no extra read. The sidecar wins for
|
|
672
|
+
// `sessionId` when both are present — the sidecar is the file the
|
|
673
|
+
// speaker system keys on.
|
|
674
|
+
const declared = mergedScribeSessions(content)
|
|
675
|
+
if (declared.length > 0) {
|
|
676
|
+
meta.derivedKind = 'merge'
|
|
677
|
+
meta.g2SessionIds = declared
|
|
678
|
+
if (!meta.sessionId) meta.sessionId = declared[0]
|
|
679
|
+
const actionId = mergedScribeActionId(content)
|
|
680
|
+
if (actionId) meta.actionId = actionId
|
|
681
|
+
}
|
|
591
682
|
if (options.day && meta.date !== options.day) continue
|
|
592
683
|
allMeetings.push(meta)
|
|
593
684
|
} catch { /* skip unreadable files */ }
|