@gotcos/glasses-server 6.94.4 → 6.98.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 +147 -0
- package/README.md +52 -0
- package/managed-runtime-contract.json +13 -0
- package/package.json +3 -1
- package/server/index.ts +19 -1
- package/server/lib/attached-provider-adapter.ts +35 -1
- package/server/lib/claude-bridge.ts +47 -0
- package/server/lib/codex-bridge.ts +11 -1
- package/server/lib/codex-rollout-watermark.ts +42 -0
- package/server/lib/cursor-bridge.ts +2 -1
- package/server/lib/dictation-clean-models.ts +2 -0
- package/server/lib/dictation-clean.ts +2 -2
- package/server/lib/dictation-polish-pipeline.ts +22 -2
- package/server/lib/dictation-prewarm.ts +16 -0
- package/server/lib/direct-bridge.ts +14 -2
- package/server/lib/direct-sse.ts +6 -1
- package/server/lib/display-bus.ts +1 -1
- package/server/lib/fork-thread.ts +32 -1
- package/server/lib/held-naming-batches.ts +36 -0
- package/server/lib/job-audit-caller.ts +20 -0
- package/server/lib/lens-gist-engines.ts +17 -5
- package/server/lib/live-cos-ask.ts +29 -6
- package/server/lib/live-cue-verdicts.ts +43 -8
- package/server/lib/live-cues-engine.ts +4 -1
- package/server/lib/live-cues-filter.ts +50 -0
- package/server/lib/live-cues-general-prompts.ts +137 -0
- package/server/lib/live-cues-general-topic.ts +245 -0
- package/server/lib/live-cues-general.ts +715 -0
- package/server/lib/live-cues-v2-data.ts +453 -0
- package/server/lib/live-cues-v2-grounded.ts +262 -0
- package/server/lib/live-cues-v2-judge-b.ts +59 -0
- package/server/lib/live-cues-v2-names.ts +49 -0
- package/server/lib/live-cues-v2-notes.ts +8 -4
- package/server/lib/live-cues-v2-open-items.ts +132 -0
- package/server/lib/live-cues-v2-pacing.ts +45 -0
- package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
- package/server/lib/live-cues-v2-settings.ts +89 -10
- package/server/lib/live-cues-v2.ts +570 -47
- package/server/lib/mac-recording-asr.ts +6 -3
- package/server/lib/mac-speaker-call-human.ts +371 -0
- package/server/lib/mac-speaker-call-interval-human.ts +499 -0
- package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
- package/server/lib/mac-speaker-call-original-view.ts +328 -0
- package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
- package/server/lib/mac-speaker-call-producer.ts +539 -0
- package/server/lib/mac-speaker-call-store.ts +264 -0
- package/server/lib/mac-speaker-call-view.ts +373 -0
- package/server/lib/mac-speaker-live-compute.ts +72 -0
- package/server/lib/mac-speaker-live-memory.ts +414 -0
- package/server/lib/mac-speaker-live-projection.ts +126 -0
- package/server/lib/mac-speaker-live-save.ts +90 -0
- package/server/lib/mac-speaker-live-store.ts +71 -0
- package/server/lib/mac-speaker-live-worker.ts +58 -0
- package/server/lib/mac-speaker-live.ts +175 -0
- package/server/lib/mac-speaker-profile-extractor.ts +15 -10
- package/server/lib/mac-speaker-shadow-repair.ts +307 -0
- package/server/lib/mac-voice-assignment.ts +44 -26
- package/server/lib/mac-voice-display.ts +195 -0
- package/server/lib/mac-voice-naming-batch.ts +1067 -0
- package/server/lib/mac-voice-suggestions.ts +548 -0
- package/server/lib/maintenance-lifecycle.ts +2 -0
- package/server/lib/media-store.ts +2 -1
- package/server/lib/meeting-ask-pasted.ts +135 -0
- package/server/lib/meeting-ask-review.ts +8 -1
- package/server/lib/meeting-ask-runner.ts +4 -1
- package/server/lib/meeting-ask-runtime.ts +189 -17
- package/server/lib/meeting-ask-store.ts +2 -0
- package/server/lib/meeting-ask-web.ts +284 -13
- package/server/lib/meeting-batch-progress.ts +51 -5
- package/server/lib/meeting-batch-transcribe.ts +35 -2
- package/server/lib/meeting-fact-check.ts +3 -0
- package/server/lib/meeting-finalization-jobs.ts +69 -2
- package/server/lib/meeting-public-topic.ts +6 -1
- package/server/lib/meeting-speaker-review.ts +182 -17
- package/server/lib/meeting-store.ts +6 -1
- package/server/lib/parakeet.ts +51 -5
- package/server/lib/pending-batch-retention.ts +58 -0
- package/server/lib/private-reference.ts +38 -0
- package/server/lib/provider-usage.ts +561 -0
- package/server/lib/public-https-url.ts +32 -0
- package/server/lib/query-job-runtime.ts +3 -0
- package/server/lib/rich-media-safety.ts +23 -2
- package/server/lib/speaker-shadow-decide.ts +503 -0
- package/server/lib/speaker-shadow-models.ts +150 -0
- package/server/lib/speaker-shadow-numeric.ts +143 -0
- package/server/lib/speaker-shadow-pair-source.ts +91 -0
- package/server/lib/speaker-shadow-paired.ts +268 -0
- package/server/lib/speaker-shadow-report.ts +113 -0
- package/server/lib/speaker-shadow-timeline.ts +210 -0
- package/server/lib/speaker-shadow-worker.ts +312 -0
- package/server/lib/speaker-shadow.ts +788 -0
- package/server/lib/token-audit.ts +36 -6
- package/server/lib/transcript-cleanup.ts +3 -2
- package/server/lib/voice-directory.ts +8 -4
- package/server/lib/web-lookup-ledger.ts +151 -0
- package/server/lib/work-review-runtime.ts +2 -0
- package/server/lib/work-search.ts +3 -1
- package/server/lib/xlsx-text.ts +277 -0
- package/server/routes/health.ts +5 -0
- package/server/routes/live-cues.ts +36 -14
- package/server/routes/meeting.ts +506 -49
- package/server/routes/speaker-shadow.ts +79 -0
- package/server/routes/transcribe-stream.ts +114 -33
- package/server/routes/voice.ts +3 -0
- package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
- package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
- package/shared/live-cues-v2.ts +38 -5
- package/shared/media-attachment.ts +2 -0
- package/shared/meeting-ask.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
+
## 6.98.0 - Live cues with five kinds, phone tuning, and a meeting assistant that reads links (public)
|
|
2
|
+
|
|
3
|
+
2026-11-03. The public release of the private 6.95.0 to 6.97.9 line, merged with everything 6.94.4 published. Pairs with COS Glasses 6.11.669 and works with COS Control 0.5.363.
|
|
4
|
+
|
|
5
|
+
- Live cues stay off until a person turns them on. Once on: G2 meetings armed by the phone get all five kinds (reinforce, challenge, decision, promise, follow-up), cues are paced across the meeting instead of spending the cap early, a cue that repeats a recent opening line is held back, and names come from the meeting's own speakers.
|
|
6
|
+
- For anyone who already has live cues on, pacing, the repeat hold, meeting speaker names sent to the cue writer, and grouped open items on Mac meetings start on install. Each has an env switch to turn it off.
|
|
7
|
+
- Cue tuning from the phone and from COS Control uses `GET/POST /api/live-cues/v2/settings?v=3`.
|
|
8
|
+
- The meeting assistant reads a public link typed in a question, runs up to three searches for a comparison or fact check and leads with a verdict, and treats "private" as a privacy word only when it means privacy. Web searches go out only with Public topics on, using only the words the user typed. A link the user types into a question is read even with Public topics off, unless the question says not to use the web. Pasted text over 400 characters and pages read from typed links are kept as meeting files for live cues.
|
|
9
|
+
- Dropped `.xlsx` files give their text to the meeting assistant.
|
|
10
|
+
- A daily web lookup count (`webToday`) on the cue settings and meeting assistant config.
|
|
11
|
+
- Saved Mac calls no longer fail on one silent window, and a meeting that has stopped retrying no longer locks Restart and Update.
|
|
12
|
+
- The token ledger records provider-reported usage where the provider gives it.
|
|
13
|
+
- Opt-in only, unchanged by default: speaker merge shadow, general cues, grounded cues, the Tableau data arm (needs a local `data-facts.json`).
|
|
14
|
+
|
|
15
|
+
## 6.97.9 - G2 meetings with all five cue kinds stay focused (private candidate)
|
|
16
|
+
|
|
17
|
+
2026-11-02. Built on 6.97.8. Pairs with the phone build that arms G2 meetings with all five kinds. Private; npm latest stays 6.94.4.
|
|
18
|
+
|
|
19
|
+
- A `meeting_` session armed with `kinds: 'extended'` gets the five cue kinds and writer v4, and nothing else of the Mac's: it is never offered to the general (web) cue runner and never gets an insight, cue mode `general` never switches its focused cues off, and the open-items fold never holds its cues (they show as written). Those three, and the insight emit path, now key on the session being a Mac recording (`mac_` id), not on the kind set.
|
|
20
|
+
|
|
21
|
+
## 6.97.8 - Live cues: Tableau facts, shared links and pacing (private candidate)
|
|
22
|
+
|
|
23
|
+
2026-11-02. Built on 6.97.7, which it carries. Pairs with COS Control 0.5.365. Private, for one Mac; npm latest stays 6.94.4.
|
|
24
|
+
|
|
25
|
+
- Grounded cues gain a data arm: a local `data-facts.json` (written by COS from the Tableau report, contract v1) offers one candidate when the recent speech names a Quilt industry, brand or SBU/XBU and a funnel word or figure. The cue states the period, marketing-sourced scope and refresh time, and never presents it as revenue, MRR or locations. Quilt meetings only: a meeting with no confirmed domain needs a brand or SBU/XBU named, a Quilt-domain memory in the same evaluation, or a Quilt term in the calendar title; another confirmed domain or an unreadable context is off. Off when the file is missing, invalid or ahead of the clock; an open month expires 120 hours after its refresh, a closed month after 35 days. Sent as a `file` source with no locator, so older clients render it. Trace: `arms.data`, `dataArm`, `dataSignal`, `dataAgeH`; the cue log marks it `dataNote`.
|
|
26
|
+
- The meeting assistant reads the same facts for funnel, budget, pacing or "check Tableau" questions, from the user's own typed words only. No network.
|
|
27
|
+
- A public link typed in a meeting question is read as a page even without "search" wording (up to two; credentials, keys, private, NAT64, IPv4-compatible, site-local, multicast, 6to4 and Teredo addresses refused). Pasted text over 400 characters and pages read from typed links are kept as meeting files for live cues, after the user's own files, with unsafe links cut, and never used as transcript-cleanup vocabulary.
|
|
28
|
+
- "Private" counts as a privacy word only when it means privacy (private notes, data, conversation, pricing, "keep this private", "in private"); private aviation, jets, equity or schools are public subjects again in the web gate, the public topic path, fact checks and general cues.
|
|
29
|
+
- Public comparison and fact-check questions get up to three parallel searches on the user's own words (plus "average" or "comparison") and four page reads, a 55 s deadline (others keep 30 s) with the second plan hop, public topic call and claim planner skipped, and an answer that leads with a verdict naming who was right, then cited figures and one caveat. When no page could be read, one line starting "Not checked against a source:" gives the general-knowledge answer (never after a privacy withhold).
|
|
30
|
+
- `.xlsx` text extraction on the media route: linear scanners, bounded by 2 s of scanning, 48 MB inflated, 12 sheets and 200 rows per sheet; no formulas.
|
|
31
|
+
- New daily web lookup ledger, `webToday` on `GET /live-cues/v2/settings?v=3` and `GET /meeting-ask/config`: searches, pages read and pages failed for cues, the assistant and fact checks, and lookups held back after the trigger, by reason. `general.today` keeps its meaning.
|
|
32
|
+
- Cue pacing: the per-meeting cap opens with half the cap, then refills across the meeting (calendar length when known, else 60 minutes; 5 minutes per cue at cap 25). Caps of 4 or less are not paced; a raised cap takes effect at once (`COS_LIVE_CUES_V2_PACING=0` turns it off).
|
|
33
|
+
- On Mac recordings, "no owner or date" cues the writer marks `open` fold into one cue per 10 minutes headed by the first item; an item left alone 20 minutes is shown on its own when the meeting continues, unless the room talked about it since; every held cue is kept in the cue log marked `held` and left out of cue history, counts and ratings. G2 cues are shown as written (`COS_LIVE_CUES_V2_OPEN_ITEMS=0`).
|
|
34
|
+
- A cue whose opening line repeats one of the last six is withheld (`COS_LIVE_CUES_V2_LEAD_REPEAT=0`). The writer gets the meeting's known names for spelling: this meeting's speakers, the owner and name-like vocabulary; enrolled voice names are never sent (`COS_LIVE_CUES_V2_KNOWN_NAMES=0`).
|
|
35
|
+
|
|
36
|
+
## 6.97.7 - Live cues: grounded cues, cue modes and general cues (private candidate)
|
|
37
|
+
|
|
38
|
+
2026-11-01. Built on 6.97.6 (SubQ session find), which it carries. Pairs with COS Control 0.5.364. Everything new is off by default: until the owner changes the cue mode or turns grounded cues on, this server answers, asks and writes exactly as 6.97.6 does.
|
|
39
|
+
|
|
40
|
+
- `GET /api/live-cues/v2/settings?v=3` and `POST {v: 3, ...}` add the cue mode (`focused`, `general`, `all`; default focused), grounded cues (`classic`, `grounded`; default classic) and six general-cue limits to the v2 body. `?v=2`, `{v: 2}` and the unversioned calls answer byte for byte as before; an older server answers `?v=3` with 400 `unsupported_version`, which is Control's signal to hide the new controls. The values live in `live-cues-v2-settings.json` (schema 3). `COS_LIVE_CUES_MODE=focused` and `COS_LIVE_CUES_GROUNDING=classic` can only force the safe value; any other value defers to the setting.
|
|
41
|
+
- Grounded cues: cue_memory.py and the owner's notes run together, the files attached to the meeting add their best matching passage, copies of this conversation and personal notes in a work meeting are dropped, one meeting is one candidate, and one judge request asks yes or no per candidate (`COS_LIVE_CUES_V2_JUDGE_YES`, default 0.65, the best two by yes are kept). The writer is told the confirmed calendar title. The trace gets the judge's probabilities, the kept ids (`keptIds`; `kept` stays a count), arm timings, an excluded count and a count of notes that named no domain, never text. A copy of this conversation is recognised against everything said since the meeting was armed (up to 50,000 6-word sequences), not only the last 400 words. With classic nothing in an evaluation changes.
|
|
42
|
+
- A cue source may carry its kind (`meeting`, `memory`, `file`, `web`) and one locator (`recordId`, `memoryId`, `fileId`, `url`, https only). The cue log accepts them, the kind `insight`, and the optional `arms` and `general` fields; `memorySource` keeps its three values. `shared/live-cues-v2.ts` changed, so the companion's byte-identical copy is stale until a companion release takes it; the companion is never sent an insight or a source it cannot read.
|
|
43
|
+
- General cues (cue mode `general` or `all`, Mac meetings only; G2 meetings keep focused cues and never get an insight; `general` turns focused cues off for Mac meetings). They need Ask's Public topics on. A server-wide runner, separate from focused cues, ticks each Mac meeting every `intervalSeconds` (default 300) once enough new speech arrived. Order: the meeting must not be confirmed personal, an orientation that cannot be read withholds, the window must not be about health, family, money, legal or school matters, and voice names must be readable; then one Jev or SubQ yes/no on whether the room is on a public topic (`trigger`, default 0.6), then a pinned Haiku call picks one short topic, then admission refuses any speaker, enrolled voice or calendar-title name, any digit or number word, any owner vocabulary term (also written without spaces), deny-list words, non-Latin script and repeats; a local topic gets the owner's city-level home area and is admitted again; then one web search (`auditCaller: live_cues_general`), then a writer whose numbers must appear in a cited source. Only the topic reaches the web, never the transcript. The cue is kind `insight`, sent as `coaching_nudge_v2` with no v1 twin, logged with `general: {category, query}`, and never touches focused cooldown, counters or breaker.
|
|
44
|
+
- General caps, durable and charged before each call: attempts and cues per meeting, topic calls, web searches and (env-only, default 300) trigger calls per local day; a breaker after 3 failures opens for an hour. The trace row `general_tick` holds numbers, a category and, only for non-privacy refusals, a query hash; never the query. `GET /api/live-cues/status?sessionId=` adds `general {state, reason, cues, attempts}` for Mac meetings.
|
|
45
|
+
- The ASR route passes each chunk's speakers to live cues (stored for the general name gate only). `end().nudgesGenerated` counts focused cues only. The topic module reads the profile lazily (a test that mocked the profile partially failed at import before).
|
|
46
|
+
|
|
47
|
+
## 6.97.6 - SubQ session find shows every session that fits (private candidate)
|
|
48
|
+
|
|
49
|
+
2026-11-01. Carries what 6.94.4 publishes, and replaces the measuring build 6.97.5. Pairs with COS Control 0.5.363.
|
|
50
|
+
|
|
51
|
+
## 6.97.4 - Last review fixes (private candidate)
|
|
52
|
+
|
|
53
|
+
2026-11-01. A private candidate for one Mac, carrying the fixes published in 6.94.3. Pairs with COS Control 0.5.363.
|
|
54
|
+
|
|
55
|
+
- Merge checks every copy of a name stored in both Unicode forms and refuses on any below the bar; delete previews count them all; renames reach meetings in either form. `?name=a&name=b` answers 400.
|
|
56
|
+
- A file reference like `meeting.ts:2452`, bare `localhost` and a bare IP are no longer treated as links.
|
|
57
|
+
|
|
58
|
+
## 6.97.3 - Speaker names and session find, from QA (private candidate)
|
|
59
|
+
|
|
60
|
+
2026-11-01. A private candidate for one Mac. Pairs with COS Control 0.5.363.
|
|
61
|
+
|
|
62
|
+
- Accented names: merge, delete-person, calibration, the rename fan-out and the embeddings manager now match a stored name in either Unicode form, a meeting record id matches in either form (default-on), and a confirm on a decomposed name records the person's confirmation. Merge refuses if any stored copy of a name is below the similarity floor, and the delete preview counts every copy. `?name=a&name=b` on enroll is a 400.
|
|
63
|
+
- Session find: a title or prompt cut inside an attachment is dropped instead of sent, Windows, volume and escaped-space home paths are masked, ordinary words like `/home/dashboard` stay, and links to an IP address or localhost with a port or path are masked. Only home-folder paths are masked, not every file path; bare domains, bare addresses and `file.ts:12` stay. Refusals log one line per reason, with no words.
|
|
64
|
+
|
|
65
|
+
## 6.97.2 - Local paths stay off the search (private candidate)
|
|
66
|
+
|
|
67
|
+
2026-11-01. A private candidate for one Mac. Pairs with COS Control 0.5.363.
|
|
68
|
+
|
|
69
|
+
- Session find drops attachment paths at the start of a first prompt, so the 150 characters sent begin at the typed words, and it masks home paths, `~/`, `file://` and `C:\Users` in text, titles and queries. On this Mac 55 of the newest 508 first prompts started with an attachment and 105 held a home path in their first 400 characters.
|
|
70
|
+
|
|
71
|
+
## 6.97.1 - Search sessions by meaning (private candidate)
|
|
72
|
+
|
|
73
|
+
2026-11-01. A private candidate for one Mac, built on 6.97.0, carrying the same commits as the public line's session find route. Pairs with COS Control 0.5.362.
|
|
74
|
+
|
|
75
|
+
- `POST /api/agent-sessions/find` asks the person's decision model (Jev or SubQ) which recent sessions a few typed words refer to, after consent for that provider, and returns up to 5 rows in order of likelihood.
|
|
76
|
+
- `GET /api/decision-provider` adds `capabilities.sessionFind`. The session list and search are unchanged.
|
|
77
|
+
- Accented speaker names now match. COS Control's helper receives names in decomposed Unicode, so "Yes, this is Brigitta Pólya" failed with "No chunk in this meeting is labelled" although her chunks carried that name. Speaker-name routes in meeting and voice now read names as NFC and compare NFC on both sides, and a new name is stored as NFC. Names already saved are not rewritten.
|
|
78
|
+
|
|
79
|
+
## 6.97.0 - COS's token ledger records what providers report, and a stopped meeting no longer locks Update (private candidate)
|
|
80
|
+
|
|
81
|
+
2026-10-31. A private candidate for one Mac, built on 6.96.4. Pairs with the COS cost view in the next COS Control build, which reads the ledger file directly; there is no new endpoint.
|
|
82
|
+
|
|
83
|
+
**Restart and Update are no longer locked by a meeting that has stopped trying.** In 6.96.4 a saved meeting waiting on HQ held `meeting_sync.active`, `meeting_sync.blocksRestart` and `finalization.pending` for as long as its job existed, up to 30 days, though retries in one process end after three attempts. COS Control disabled Restart, Update Server and Sync now on those, so a fix could not reach the Mac.
|
|
84
|
+
- A job that has used its retries now records that it stopped, in its own job file, and counts in none of those three. It stays visible as a `meeting_sync.meetings` row with phase `stopped` and a label that says so. `finalization` keeps its shape, and an older COS Control unlocks too, because `pending` reads 0. Its audio and job file stay. Nothing is deleted.
|
|
85
|
+
- A job that is working still blocks: while an attempt runs, and between its retries.
|
|
86
|
+
- A later start does not take the lock back every time. A stopped job is tried once, with no retry timers, on a start of a different server version or a start 6 hours or more after it stopped. Its lock lasts as long as that one attempt runs, and it is the same window as the maintenance lease. Any other start runs nothing and holds nothing.
|
|
87
|
+
- A job that failed before this version recorded stops is tried once at its first start on 6.97.0, then stops the same way. A job the boot gate parked for crash-looping also stops holding the lock; it keeps the gate's own second chance and Retry.
|
|
88
|
+
|
|
89
|
+
**The ledger.**
|
|
90
|
+
- A token-audit row now carries real usage when the provider reported it: `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `cache_write_1h_tokens`, `cost_usd` and `resolved_model`, with `usage_kind` `provider_final`. That label now needs real numbers. All four counts are always written. `cache_write_1h_tokens` is part of `cache_write_tokens`; it and `cost_usd` appear only when the provider states them.
|
|
91
|
+
- A row with none of these keeps every existing field byte for byte, so the Python readers are unaffected. Polish rows keep `input_chars` as the words dictated and add `prompt_chars`: what the model is sent, system prompt included.
|
|
92
|
+
- What the numbers mean differs by provider. `input_tokens` never includes cache reads. It excludes cache writes only on Claude, the one provider that reports them. Codex's write field read 0 on all 31,670 rollout rows checked, and the direct lane reads none, so on both `cache_write_tokens` is 0 and any write stays in `input_tokens`. `resolved_model` is what the CLI says ran on Claude, and what COS asked for on Codex, direct and Ollama.
|
|
93
|
+
- Real where a provider states it: the lens gist, cues and transcript cleanup; Meeting Ask; warm dictation polish; the Claude, Codex and direct bridges and the boot pre-warm; Claude attached and fork runs.
|
|
94
|
+
- Still estimated: cold polish, whose plain text output has no usage (JSON there would add failures that feed the breaker); every Cursor path, whose cache parser has never produced a real row; Codex attached and fork runs, which share a thread with the desktop; and summaries that run through `spawnClaudeText`.
|
|
95
|
+
- A resumed turn is charged only itself. Claude's `total_cost_usd` and `modelUsage` and Codex's `usage` are running totals that carry earlier turns of the session. Claude reads `model_usage_since_last_result`, and each result counts once however many a steered run writes. Usage on a result that arrives after the row is written gets one `:late_result` row under the same run id. On a CLI without that field, a resumed Claude run reports this turn's tokens and no cost.
|
|
96
|
+
- Codex charges the difference from the last total this bridge saw on the thread, and only when it can show nothing else ran there since. The row stays estimated on the first resumed turn after a restart, after a turn that never reported a total, after an attached run, and when the thread's rollout grew between turns, which is how a desktop run shows. The turn after is real again.
|
|
97
|
+
- Every row these paths log now names its run and provider session where known: `run_id` and `cli_session_id`. A provider id that has spaces, or is shaped like a key or a path, is refused rather than written.
|
|
98
|
+
- Dispatched tasks and meeting reviews get their own caller, `dispatch_task` and `work_review`, in place of `full_query`. Attached and fork runs that reach an end get a row for the first time: source `g2-attached`, caller `attached_turn`; source `g2-fork`, caller `fork_thread`.
|
|
99
|
+
- A missing or malformed usage block changes nothing: same answer, no failure, no breaker count, no retry, and the row stays estimated. No prompt or answer text enters the audit.
|
|
100
|
+
- The test config now points the audit at a scratch file (`COS_TOKEN_AUDIT_FILE`), so a test's made-up numbers cannot reach a real ledger. Tests that did not mock the audit used to append to the home file.
|
|
101
|
+
|
|
102
|
+
## 6.96.4 - Saved Mac calls no longer stick on one empty window (private candidate)
|
|
103
|
+
|
|
104
|
+
2026-10-31. A private candidate for one Mac, built on 6.96.3 with nothing else in it.
|
|
105
|
+
|
|
106
|
+
- A saved Mac or iPhone call no longer fails because Parakeet heard no words in one window over audio the speech detector counts as speech. The window is treated as an answer: it keeps any live words it had, stays empty if it had none, and says so in that window's `stt.fallbackReason`. Before, that one window failed the whole call, and every retry failed the same way.
|
|
107
|
+
- A real Parakeet failure (a CLI exit, a timeout, a busy lane) still blocks the call and retries, as it did.
|
|
108
|
+
- An error that never reached the CLI is no longer reported as `cli_failed`. It now reads `internal`, and the log names the window and the reason.
|
|
109
|
+
- Cleanup no longer deletes the pending audio of a saved meeting still waiting on HQ, for up to 30 days. Before, it went 12 hours after the last attempt, and the next start finished the meeting on its streaming text.
|
|
110
|
+
|
|
111
|
+
## 6.96.3 - Fewer voices in Mac speaker review, and suggested names on request (private candidate)
|
|
112
|
+
|
|
113
|
+
2026-10-30. A private candidate for one Mac. Pairs with the matching private COS Control build.
|
|
114
|
+
|
|
115
|
+
- Speaker review can show a Mac recording's voices as merged groups first, then a row for each voice with 15 seconds or more, then one expandable row for the shorter ones. It reads the merges speaker shadow already logged for that meeting. Display only: no label, name, sample or file changes, and every voice's words stay one click away. A voice named since the log was written leaves its group. A Mac with speaker shadow off sees no change.
|
|
116
|
+
- A new on-click job compares each of a meeting's voices with the shared voice profiles, one at a time, using the rule frozen in the October 30 study (top score .70 or more, margin .05 or more). Voices in a merged group share one suggestion only when each passed on its own for the same person. On the 12 voices judged by ear it reproduced the study: 10 right, 0 wrong, 2 with no suggestion. That is three people, not proof across every profile, and suggestions on other voices are unverified. A result expires when the meeting or the voice profiles change.
|
|
117
|
+
- A suggestion is never a name. Those scoring .80 or more with a .05 margin are marked high confidence, and they are still suggestions. A person confirms them, one at a time or as a previewed batch, through the same writes as naming a voice by hand. The job writes nothing, keeps its result in memory only, never suggests the owner, ignores samples taken from the same recording, and refuses during a recording or maintenance.
|
|
118
|
+
- A batch stops at the first refusal and reports what was named. Undo reverses the whole batch and removes only the voice samples it added, so names set one at a time keep theirs. A voice a person changed afterward is left alone and reported. A batch never names the owner.
|
|
119
|
+
- Automatic names are unchanged.
|
|
120
|
+
|
|
121
|
+
## 6.96.2 - Source-bound anonymous speaker review (private local candidate)
|
|
122
|
+
|
|
123
|
+
2026-10-30. Pairs with private COS Control 0.5.357 / build 411 for the interval review. Existing clients retain their compatible review path.
|
|
124
|
+
|
|
125
|
+
- A separately qualified, source-bound whole-call packet can project anonymous groups onto fully supported timed words. Gaps, transitions and unsupported speech remain unresolved. This derived review does not rewrite the canonical meeting, correction ledger, names, profiles, words or timestamps.
|
|
126
|
+
- The review separates supported anonymous groups, existing named rows and unresolved speech. These rows are not a count of people. Fragment counts and old root-level phrases do not stand in for interval evidence.
|
|
127
|
+
- Human review supports exact timed-word naming, renaming and Undo while the automatic producer is off. Each action is bound to the reviewed source revision; prior corrections and protected words take precedence. For this private candidate, a name already asserted elsewhere in the source is refused rather than risking attribution of unrelated words.
|
|
128
|
+
- A recording can return to its original review after its interval naming actions are undone. Interrupted transactions require recovery before another action; packet and state changes invalidate stale review tokens. Original files, earlier corrections and voice profiles remain untouched.
|
|
129
|
+
- Held-naming Undo proceeds separately for each meeting: a call-wide review can block its own meeting without preventing unrelated meetings in the batch from reverting.
|
|
130
|
+
- The accepted-only October 30 repair covers 3,423 previously unassigned timed words in four anonymous groups, leaves 9,471 words unresolved, and preserves 253 named words. Whole-call human comparison checks on two earlier calls found no conflicting names within this narrower accepted support; identity accuracy on the current call remains unconfirmed. Broader interval expansion failed that quality gate and remains excluded.
|
|
131
|
+
- Includes the 6.96.1 bounded save-receipt lookup correction for large recording sidecars; retained audio and saved-meeting receipts remain separate from speaker quality.
|
|
132
|
+
- Transcript cleanup, live cues and live speaker ID remain disabled. Automatic interval production, profile naming and enrollment are not enabled by this candidate. Historical repair requires the exact qualified packet and correction-preservation checks.
|
|
133
|
+
|
|
134
|
+
## 6.95.0 - Speaker merge shadow mode (opt-in, log only)
|
|
135
|
+
|
|
136
|
+
2026-10-28. Compatible with COS Control 0.5.352; no Control or glasses change required.
|
|
137
|
+
|
|
138
|
+
- **Why:** Mac recordings split one person into many anonymous `Voice N` labels (45 on one 60-minute call). The research canary (`community-1-fluidaudio-v1`, rounds 1 to 3) found a hybrid that fixes this safely on the reference call: production labels stay the base, and FluidAudio's offline Community-1 diarizer only proposes merges behind four gates. On the Oct 27 reference call (11 human-confirmed people) it cut fragmentation from 27 to 18 with no mixed people and the error proxy from 33% to 3%. Before any write path exists, the proposals have to be scored against Miles's own corrections, so this release only computes and logs them.
|
|
139
|
+
- **What:** with shadow mode on, every Mac recording whose finalization completes is queued. When no recording, save, finalization, one-shot transcription or dictation is live, a niced child process rebuilds the whole-call timeline from the retained chunk WAVs (each verified against the sidecar's `audioSha256`), runs the Parakeet install's `fluidaudiocli process --mode offline --overlapping-segments --export-embeddings` under `sandbox-exec` with network denied, applies the research gates (a port of `hybrid.decide()`), and appends one record per meeting to `<data>/speaker-shadow/proposals.jsonl`: accepted and rejected proposals with reasons, gate values, the post-hoc anchor ratio (reported, never decisive), co-occurring named labels as hints, and listen pointers by chunk and time (no transcript text). A readable report per finished meeting lands in `<data>/speaker-shadow/reports/<YYYY-MM-DD>/<title>__<session>.md`: "N voices would become M", each merge with its gate values and listen pointers (chunk file, time and words), rejections with reasons, and name hints ("this group also holds 65 s labelled ..."). `GET /api/speaker-shadow/proposals` and `/status` read them (token required); while it is on, `/api/health` reports `speaker_shadow` (absent when off, so the health shape is unchanged by default).
|
|
140
|
+
- **Log only:** nothing writes a meeting file, label, name, profile, embedding cache or correction ledger. Pinned by a data-home snapshot test (every file byte-identical after a run), an import allowlist and pinned write-call counts for every shadow module, and mutation-tested.
|
|
141
|
+
- **Exact port:** the TypeScript gates reproduce the Python tool's Driving decisions exactly (45 voices to 27, 29 proposed, 18 accepted, 11 rejected, every reason string and unrounded outlier distance bit for bit), and a real-CLI run of the TypeScript pipeline on the research copies reproduces the timeline PCM and WAV SHA-256 and the full round-3 report with zero differences. Python's half-to-even rounding and %-formatting, CPython 3.12+ compensated `sum()`, dict order and code-point string order are replicated and tested against CPython 3.15 vectors.
|
|
142
|
+
- **Switch, per Mac:** `POST /api/speaker-shadow/settings {"enabled":true}` (token required, no restart) saves it in `<data>/speaker-shadow/settings.json`; `COS_SPEAKER_SHADOW=1|0` pins it instead (409 `env_forced`); otherwise it is off. The package default is off for everyone, so installing this version changes nothing until a Mac turns it on.
|
|
143
|
+
- **How it stops:** the switch (default off; off creates nothing; off mid-run stops the run), a daily cap (12), a breaker (3 failures pause 1 hour), a 180 s wall clock and an RSS cap (3 GB) on the whole process group, and live work, which defers it or stops a run mid-way and requeues the meeting. No LLM; no network at run time.
|
|
144
|
+
- **Models:** never downloaded by the server. The pinned `FluidInference/speaker-diarization-coreml@df2625ac` files must be present (`models_missing` is logged otherwise); every file is hashed before every run and linked read-only into the CLI's private HOME.
|
|
145
|
+
- **Known limits:** a recording's first run with a cold CoreML cache leaves up to four empty `NSIRD_fluidaudiocli_*` folders in the per-user temp dir (counted in the record, not removed). Recovered (orphan) Mac captures are not queued. The gates were fixed on one truth-labeled call; scoring the logged proposals is the next step.
|
|
146
|
+
- **Switches:** the per-Mac setting and `COS_SPEAKER_SHADOW` (default off), `COS_SPEAKER_SHADOW_DAILY_RUNS`, `COS_SPEAKER_SHADOW_TIMEOUT_MS` (may only lower 180 s), `COS_SPEAKER_SHADOW_RSS_MB`, `COS_SPEAKER_SHADOW_MODELS`. No default-on change.
|
|
147
|
+
|
|
1
148
|
## 6.94.4 - SubQ session find shows every session that fits
|
|
2
149
|
|
|
3
150
|
2026-11-01. Pairs with COS Control 0.5.363. Only affects people who chose SubQ and said yes to find sessions by meaning. Jev and everything else are unchanged.
|
package/README.md
CHANGED
|
@@ -726,6 +726,58 @@ limits above: this release does not promise meeting-wide speaker deduplication
|
|
|
726
726
|
or an attendee count. Older recordings require an explicit, validated repair;
|
|
727
727
|
they are not silently rewritten on upgrade.
|
|
728
728
|
|
|
729
|
+
### Speaker merge shadow mode (6.95.0, off by default, log only)
|
|
730
|
+
|
|
731
|
+
Mac recordings can split one person across many `Voice N` labels. Shadow mode asks
|
|
732
|
+
FluidAudio's offline Community-1 diarizer which of those labels belong together and
|
|
733
|
+
**logs** the answer for every finished Mac recording, with a readable report per meeting,
|
|
734
|
+
so the proposals can be checked by ear and against later human corrections. It changes
|
|
735
|
+
nothing: no meeting file, label, name, voice profile, embedding cache or correction ledger
|
|
736
|
+
is written.
|
|
737
|
+
|
|
738
|
+
- **On, for this Mac only** (no restart; the token is `COS_API_TOKEN` in `~/.cos-glasses/.env`):
|
|
739
|
+
|
|
740
|
+
```sh
|
|
741
|
+
curl -s -X POST http://127.0.0.1:3141/api/speaker-shadow/settings \
|
|
742
|
+
-H "X-Cos-Token: $(sed -n 's/^COS_API_TOKEN=//p' ~/.cos-glasses/.env)" \
|
|
743
|
+
-H 'Content-Type: application/json' -d '{"enabled":true}'
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Off:** the same with `{"enabled":false}`. The setting lives in
|
|
747
|
+
`~/.cos-glasses/data/speaker-shadow/settings.json`. `COS_SPEAKER_SHADOW=1` or `=0` in the
|
|
748
|
+
environment pins it instead (the POST then answers `409 env_forced`). With neither, it is
|
|
749
|
+
off, and off does nothing at all: no queue, no process, no file. Meetings already finished
|
|
750
|
+
when it is turned on are not queued; turning it off mid-run stops the run.
|
|
751
|
+
- **Needs:** the Parakeet install's verified `fluidaudiocli` 0.17.4, `sandbox-exec`, and the
|
|
752
|
+
pinned models from `FluidInference/speaker-diarization-coreml` at revision `df2625ac` in
|
|
753
|
+
`~/.cos-glasses/data/speaker-shadow/models/speaker-diarization/` (or
|
|
754
|
+
`COS_SPEAKER_SHADOW_MODELS`). The server never downloads them: missing or mismatched
|
|
755
|
+
models are logged as `models_missing` or `models_mismatch`. `GET /api/speaker-shadow/status`
|
|
756
|
+
shows `readiness` (models present, CLI installed and how it is trusted, sandbox). The models
|
|
757
|
+
are CC-BY-4.0 (scoped); see the repo's NOTICE.md.
|
|
758
|
+
- **When:** after a Mac recording's finalization is complete. A live recording, save,
|
|
759
|
+
batch finalization, one-shot transcription or dictation defers it, and one that starts
|
|
760
|
+
mid-run stops the run and puts the meeting back in the queue.
|
|
761
|
+
- **How it stops:** the switch, a daily cap (`COS_SPEAKER_SHADOW_DAILY_RUNS`, default 12), a
|
|
762
|
+
breaker (3 failures in a row pause it for 1 hour), a 180 s hard wall clock
|
|
763
|
+
(`COS_SPEAKER_SHADOW_TIMEOUT_MS` may only lower it) and an RSS cap on the whole run
|
|
764
|
+
(`COS_SPEAKER_SHADOW_RSS_MB`, default 3072). It runs in a niced child process, never on the
|
|
765
|
+
server's event loop. No LLM, and the CLI runs under `sandbox-exec` with network denied.
|
|
766
|
+
- **Gates** (the research tool's, unchanged): at least 0.8 of a voice's covered speech on one
|
|
767
|
+
cluster with at least half of it covered, at least 15 s of speech, and the voice's
|
|
768
|
+
embedding centroid inside the cluster's 95th-percentile spread (leave-fragment-out). Named
|
|
769
|
+
labels and `Ext` are never merged into or out of. The anchor ratio is reported for
|
|
770
|
+
scoring and decides nothing.
|
|
771
|
+
- **Read:** one report per finished Mac meeting in
|
|
772
|
+
`~/.cos-glasses/data/speaker-shadow/reports/<YYYY-MM-DD>/<title>__<session>.md`: "N voices
|
|
773
|
+
would become M", each accepted merge with its seconds, gate values and listen pointers
|
|
774
|
+
(chunk file, time, the chunk's words), rejections with reasons, and named labels sharing a
|
|
775
|
+
group as a hint only ("this group also holds 65 s labelled ..."). A meeting with no
|
|
776
|
+
proposals gets a one-line page saying why. The records are in `proposals.jsonl` beside it
|
|
777
|
+
(rotated past 8 MB) and `GET /api/speaker-shadow/proposals?limit=10&sessionId=mac_<uuid>`
|
|
778
|
+
(token required). While it is on, `/api/health` carries `speaker_shadow` (codes and counts
|
|
779
|
+
only); when off the key is absent.
|
|
780
|
+
|
|
729
781
|
### iPhone microphone meetings (6.93.0, off by default)
|
|
730
782
|
|
|
731
783
|
With `COS_IPHONE_MICROPHONE_MEETINGS=1`, HQ set to Parakeet Ultra and Parakeet
|
|
@@ -36,6 +36,19 @@
|
|
|
36
36
|
"COS_LIVE_CUES_V2_MAX_PER_MEETING",
|
|
37
37
|
"COS_LIVE_CUES_V2_DAILY_WRITER_CALLS",
|
|
38
38
|
"COS_LIVE_CUES_V2_MAC",
|
|
39
|
+
"COS_LIVE_CUES_V2_JUDGE_YES",
|
|
40
|
+
"COS_LIVE_CUES_V2_LEAD_REPEAT",
|
|
41
|
+
"COS_LIVE_CUES_V2_KNOWN_NAMES",
|
|
42
|
+
"COS_LIVE_CUES_V2_PACING",
|
|
43
|
+
"COS_LIVE_CUES_V2_OPEN_ITEMS",
|
|
44
|
+
"COS_LIVE_CUES_MODE",
|
|
45
|
+
"COS_LIVE_CUES_GROUNDING",
|
|
46
|
+
"COS_LIVE_CUES_GENERAL_INTERVAL_S",
|
|
47
|
+
"COS_LIVE_CUES_GENERAL_TRIGGER",
|
|
48
|
+
"COS_LIVE_CUES_GENERAL_ATTEMPTS",
|
|
49
|
+
"COS_LIVE_CUES_GENERAL_MAX_PER_MEETING",
|
|
50
|
+
"COS_LIVE_CUES_GENERAL_DAILY_TOPIC_CALLS",
|
|
51
|
+
"COS_LIVE_CUES_GENERAL_DAILY_WEB_SEARCHES",
|
|
39
52
|
"COS_TAILSCALE_NUDGE",
|
|
40
53
|
"COS_MAC_VOICE_LABELS",
|
|
41
54
|
"COS_MAC_LIVE_VOICES",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.98.0",
|
|
4
4
|
"description": "COS Glasses: 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": {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"!server/scripts/routines-mutation-gate.mjs",
|
|
60
60
|
"!server/scripts/routines-canary.ts",
|
|
61
61
|
"!server/scripts/stt-mutation-gate.mjs",
|
|
62
|
+
"!server/scripts/speaker-shadow-mutation-gate.mjs",
|
|
62
63
|
"!server/scripts/dictation-stt-mutation-gate.mjs",
|
|
63
64
|
"!server/scripts/pairing-mutation-gate.mjs",
|
|
64
65
|
"!server/scripts/away-hold-mutation-gate.mjs",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"!server/scripts/transcript-cleanup-mutation-gate.mjs",
|
|
78
79
|
"!server/scripts/live-cues-v2-notes-mutation-gate.mjs",
|
|
79
80
|
"!server/scripts/live-cues-verdicts-mutation-gate.mjs",
|
|
81
|
+
"!server/scripts/live-cues-general-mutation-gate.mjs",
|
|
80
82
|
"!server/scripts/key-lanes-p1-mutation-gate.mjs",
|
|
81
83
|
"!server/scripts/key-lanes-p2-mutation-gate.mjs",
|
|
82
84
|
"!server/scripts/key-lanes-p3-mutation-gate.mjs",
|
package/server/index.ts
CHANGED
|
@@ -74,7 +74,8 @@ import { ForkJobLedger, fileForkJobPersistence } from './lib/fork-job-ledger.js'
|
|
|
74
74
|
import { nativeHead, realNativeHeadDeps, registerClaudeTranscriptHint } from './lib/native-head.js'
|
|
75
75
|
import { threadOccupancy, holderActivity } from './lib/thread-occupancy.js'
|
|
76
76
|
import { displayRouter } from './routes/display.js'
|
|
77
|
-
import { transcribeStreamRouter, getActiveTranscriptionSessionCount } from './routes/transcribe-stream.js'
|
|
77
|
+
import { transcribeStreamRouter, getActiveTranscriptionSessionCount, getTranscriptionSessionLiveness } from './routes/transcribe-stream.js'
|
|
78
|
+
import { installCodexRolloutProbe } from './lib/codex-rollout-watermark.js'
|
|
78
79
|
import { meetingRouter, resumeAutonomousMeetingSaves, resumeMeetingFinalizationJobs, scheduleTranscriptCleanupSweep, transcriptCleanupRouteDeps } from './routes/meeting.js'
|
|
79
80
|
import { createTranscriptCleanupRouter } from './routes/transcript-cleanup.js'
|
|
80
81
|
import { meetingsRouter } from './routes/meetings.js'
|
|
@@ -110,6 +111,8 @@ import { voiceRouter } from './routes/voice.js'
|
|
|
110
111
|
import { glossaryRouter } from './routes/glossary.js'
|
|
111
112
|
import { homeAreaRouter } from './routes/home-area.js'
|
|
112
113
|
import { createSttSettingsRouter } from './routes/stt-settings.js'
|
|
114
|
+
import { createSpeakerShadowRouter } from './routes/speaker-shadow.js'
|
|
115
|
+
import { configureSpeakerShadow, speakerShadowLeaseActive } from './lib/speaker-shadow.js'
|
|
113
116
|
import { setRecordingLiveProbe, startPreviewWarmLoop, stopPreviewWarmLoop } from './lib/stt-runtime.js'
|
|
114
117
|
import { findPinnedCli, killParakeetChildren, sweepParakeetStaging, sweepStaleParakeetTmp } from './lib/parakeet.js'
|
|
115
118
|
import { handoffsRouter } from './routes/handoffs.js'
|
|
@@ -167,6 +170,7 @@ import { createWorkLensRoundsRouter } from './routes/work-lens-rounds.js'
|
|
|
167
170
|
import { startWorkOrchestrator } from './lib/work-orchestrator/runtime.js'
|
|
168
171
|
import { createWorkSearchRouter } from './routes/work-search.js'
|
|
169
172
|
import { isReadOnlyApiPost } from './lib/work-search.js'
|
|
173
|
+
import { isMacVoiceNamingBatchPost } from './lib/mac-voice-naming-batch.js'
|
|
170
174
|
import { dataPath } from './lib/data-dir.js'
|
|
171
175
|
import { loadJsonOrQuarantine, durableAtomicWriteFileSync } from './lib/atomic-fs.js'
|
|
172
176
|
import { BOOT_RESUME_LEDGER_FILE, createBootResumeGate, installBootResumeGate } from './lib/boot-resume.js'
|
|
@@ -444,6 +448,8 @@ app.use('/api', (req, res, next) => {
|
|
|
444
448
|
|| req.path.startsWith('/maintenance/drain')
|
|
445
449
|
// 6.83.0 lens plans: each write takes the api_mutation lease itself and answers the contract's 503 `maintenance`.
|
|
446
450
|
|| req.path.startsWith('/work/chat/lens-')
|
|
451
|
+
// 6.96.3 voice naming batches: each write holds api_mutation itself; a drain gets the route's own refusal.
|
|
452
|
+
|| isMacVoiceNamingBatchPost(req.path)
|
|
447
453
|
if (lifecycleOwned) return next()
|
|
448
454
|
|
|
449
455
|
// COS Control must prove the candidate while a committed cross-boot gate is
|
|
@@ -1235,6 +1241,8 @@ app.use('/api', voiceRouter)
|
|
|
1235
1241
|
app.use('/api', glossaryRouter)
|
|
1236
1242
|
app.use('/api', homeAreaRouter)
|
|
1237
1243
|
app.use('/api', createSttSettingsRouter({ recordingLive: () => getActiveTranscriptionSessionCount() > 0 }))
|
|
1244
|
+
// 6.95.0: speaker merge shadow mode, read only (GET proposals and status).
|
|
1245
|
+
app.use('/api', createSpeakerShadowRouter())
|
|
1238
1246
|
app.use('/api', handoffsRouter)
|
|
1239
1247
|
app.use('/api', recoveryRouter)
|
|
1240
1248
|
app.use('/api', promptEditRouter)
|
|
@@ -1509,6 +1517,8 @@ listenRequiredServers(listeners).then(() => {
|
|
|
1509
1517
|
if (admittedRuntimeStarted) return
|
|
1510
1518
|
admittedRuntimeStarted = true
|
|
1511
1519
|
|
|
1520
|
+
// 6.97.0: lets the Codex bridge show nothing else ran on a thread between its turns.
|
|
1521
|
+
installCodexRolloutProbe()
|
|
1512
1522
|
// 6.78.0: before any boot resume. Was the last exit clean? Only boots after a crash count.
|
|
1513
1523
|
bootResume.openBoot()
|
|
1514
1524
|
// Resume already-saved meetings whose post-response HQ/operations work
|
|
@@ -1547,6 +1557,14 @@ listenRequiredServers(listeners).then(() => {
|
|
|
1547
1557
|
// defer while a capture is live or admissions are closed.
|
|
1548
1558
|
startMeetingMergeScheduler()
|
|
1549
1559
|
|
|
1560
|
+
// 6.95.0: speaker merge shadow mode (off unless COS_SPEAKER_SHADOW=1; log only). Live
|
|
1561
|
+
// work is a recording that is still live, or any capture or dictation lease; a drain
|
|
1562
|
+
// or shutdown also holds it. Until this runs, the runner never starts a diarization.
|
|
1563
|
+
configureSpeakerShadow({
|
|
1564
|
+
captureActive: () => getTranscriptionSessionLiveness().live > 0 || speakerShadowLeaseActive(),
|
|
1565
|
+
admissionsOpen: () => !gracefulShutdownStarted && maintenanceAdmissionsOpen(),
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1550
1568
|
// AFTER the imports sweep, not before: the sweep removes a killed write's
|
|
1551
1569
|
// debris, and re-driving a pending apply into a half-written decision is
|
|
1552
1570
|
// exactly what the sweep exists to prevent. A pending action is one a
|
|
@@ -106,6 +106,8 @@ import { cursorIsolationUnavailableError, cursorSpawnEnv, isCursorIsolationUnava
|
|
|
106
106
|
import type { PermissionAllowance } from './banned-permission-args.js'
|
|
107
107
|
import { anyKeyedSessions, isKeyedSession } from './keyed-sessions.js'
|
|
108
108
|
import { claudeKeySecretsGuardArgs } from './key-secrets-guard.js'
|
|
109
|
+
import { forgetCodexThreadTotals, SideRunTap } from './provider-usage.js'
|
|
110
|
+
import { logTokenAudit, type TokenAuditEntry } from './token-audit.js'
|
|
109
111
|
import {
|
|
110
112
|
continueAllowanceFor,
|
|
111
113
|
validateContinuePlan,
|
|
@@ -333,6 +335,8 @@ export interface AttachedTurnDeps {
|
|
|
333
335
|
* would not have performed.
|
|
334
336
|
*/
|
|
335
337
|
preflight: () => AttachedPreflightVerdict
|
|
338
|
+
/** 6.97.0: where the run's audit row goes. Only production wiring sets it, so no test writes a real ledger. */
|
|
339
|
+
auditRun?: (entry: TokenAuditEntry) => void
|
|
336
340
|
resolveBinary: (provider: AttachedProvider) => BinaryResolution
|
|
337
341
|
spawn: (request: AttachedSpawnRequest) => AttachedChildProcess
|
|
338
342
|
/** MUST be the measured kernel start. See the header. */
|
|
@@ -961,6 +965,8 @@ async function run(
|
|
|
961
965
|
if (request.abortSignal?.aborted === true) {
|
|
962
966
|
return fail('cancelled', 'not_attempted', { ...base, detail: 'before_spawn', reaped: true, durationMs: duration() })
|
|
963
967
|
}
|
|
968
|
+
// 6.97.0: this run moves the thread's running total without the bridge seeing it.
|
|
969
|
+
if (provider === 'codex') forgetCodexThreadTotals(nativeThreadId)
|
|
964
970
|
let child: AttachedChildProcess
|
|
965
971
|
try {
|
|
966
972
|
child = deps.spawn({ binaryPath, args, cwd, env: buildAttachedEnv(), provider })
|
|
@@ -1013,8 +1019,14 @@ async function run(
|
|
|
1013
1019
|
})
|
|
1014
1020
|
}
|
|
1015
1021
|
|
|
1022
|
+
// 6.97.0: a passive reader, wired after the run's own listeners, for one audit row at the end.
|
|
1023
|
+
const usageTap = deps.auditRun ? new SideRunTap(provider) : null
|
|
1016
1024
|
try {
|
|
1017
|
-
|
|
1025
|
+
const running = driveChild({ child, pid, deps, provider, nativeThreadId, prompt, timeoutMs, startedAt, abortSignal: request.abortSignal })
|
|
1026
|
+
usageTap?.attach(child.stdout as any)
|
|
1027
|
+
const result = await running
|
|
1028
|
+
auditAttachedRun(deps, usageTap, result, provider, nativeThreadId, prompt)
|
|
1029
|
+
return result
|
|
1018
1030
|
} finally {
|
|
1019
1031
|
// Every path: success, mismatch, non-zero exit, timeout, throw. A leaked
|
|
1020
1032
|
// entry lets a recycled pid inherit our self-ownership claim.
|
|
@@ -1022,6 +1034,27 @@ async function run(
|
|
|
1022
1034
|
}
|
|
1023
1035
|
}
|
|
1024
1036
|
|
|
1037
|
+
/** One row for a run that reached an end: a verified exit, or a result the provider wrote before failing. */
|
|
1038
|
+
function auditAttachedRun(deps: AttachedTurnDeps, tap: SideRunTap | null, result: AttachedTurnResult, provider: AttachedProvider, nativeThreadId: string, prompt: string): void {
|
|
1039
|
+
try {
|
|
1040
|
+
if (!tap || !deps.auditRun) return
|
|
1041
|
+
const realUsage = tap.finish()
|
|
1042
|
+
if (!result.ok && !tap.sawEnd) return
|
|
1043
|
+
deps.auditRun({
|
|
1044
|
+
source: 'g2-attached',
|
|
1045
|
+
caller: result.ok ? 'attached_turn' : `attached_turn:${result.reason}`,
|
|
1046
|
+
model: provider,
|
|
1047
|
+
inputChars: prompt.length,
|
|
1048
|
+
outputChars: tap.outputChars,
|
|
1049
|
+
durationMs: result.durationMs,
|
|
1050
|
+
cliSessionId: result.returnedNativeId ?? nativeThreadId,
|
|
1051
|
+
...(realUsage ? { realUsage } : {}),
|
|
1052
|
+
})
|
|
1053
|
+
} catch {
|
|
1054
|
+
// The audit never changes a turn's outcome.
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1025
1058
|
function safeTerminate(deps: AttachedTurnDeps, child: AttachedChildProcess, signal: NodeJS.Signals): void {
|
|
1026
1059
|
try {
|
|
1027
1060
|
deps.terminate(child, signal)
|
|
@@ -1870,6 +1903,7 @@ export function realAttachedTurnDeps(preflight: () => AttachedPreflightVerdict):
|
|
|
1870
1903
|
return {
|
|
1871
1904
|
now: () => Date.now(),
|
|
1872
1905
|
preflight,
|
|
1906
|
+
auditRun: entry => logTokenAudit(entry),
|
|
1873
1907
|
resolveBinary: provider => resolveProviderBinary(provider),
|
|
1874
1908
|
spawn: request => {
|
|
1875
1909
|
// 6.53.3: read BEFORE the spawn call, so every process of this turn starts at or
|
|
@@ -13,6 +13,7 @@ import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
|
13
13
|
import { cleanupModelImageInputs, type ModelImageInput } from './model-image-input.js'
|
|
14
14
|
import { resolveProviderWorkDir } from './launch-dir.js'
|
|
15
15
|
import { logTokenAudit } from './token-audit.js'
|
|
16
|
+
import { ClaudeRunUsage } from './provider-usage.js'
|
|
16
17
|
import { buildSystemPrompt, buildLightweightSystemPrompt, buildPrewarmSystemPrompt, getCachedContextInstant } from './context-builder.js'
|
|
17
18
|
import { getHistory, addExchange, setExchangeAttachments, removeExchange, formatHistoryForPrompt, getOrCreateSession, isNewSession, markSessionNotified, getSessionModel, getSessionRaw, replaceLastExchangeWithSummary, type ModelPreference, type PromptReference } from './conversation.js'
|
|
18
19
|
import { notifySessionStart, notifyExchange } from './telegram-notify.js'
|
|
@@ -263,6 +264,7 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
263
264
|
})
|
|
264
265
|
|
|
265
266
|
let buffer = ''
|
|
267
|
+
const prewarmUsage = new ClaudeRunUsage(false)
|
|
266
268
|
|
|
267
269
|
proc.stdout.on('data', (chunk: Buffer) => {
|
|
268
270
|
buffer += chunk.toString()
|
|
@@ -274,6 +276,7 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
274
276
|
if (!trimmed) continue
|
|
275
277
|
try {
|
|
276
278
|
const event = JSON.parse(trimmed)
|
|
279
|
+
prewarmUsage.observe(event)
|
|
277
280
|
if (event.type === 'result' && event.session_id && !claudeResultErrorMessage(event)) {
|
|
278
281
|
preWarmedCliSessionId = event.session_id
|
|
279
282
|
scheduleCliSessionSave()
|
|
@@ -287,6 +290,7 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
287
290
|
proc.on('close', () => {
|
|
288
291
|
preWarmInProgress = false
|
|
289
292
|
const elapsed = Date.now() - start
|
|
293
|
+
const prewarmRealUsage = prewarmUsage.take()
|
|
290
294
|
logTokenAudit({
|
|
291
295
|
source: 'g2-prewarm',
|
|
292
296
|
model: DEFAULT_MODEL,
|
|
@@ -294,6 +298,8 @@ export async function preWarmCLI(): Promise<void> {
|
|
|
294
298
|
outputChars: 50,
|
|
295
299
|
durationMs: elapsed,
|
|
296
300
|
caller: 'prewarm',
|
|
301
|
+
...(prewarmRealUsage ? { realUsage: prewarmRealUsage } : {}),
|
|
302
|
+
...(prewarmUsage.cliSessionId ? { cliSessionId: prewarmUsage.cliSessionId } : {}),
|
|
297
303
|
})
|
|
298
304
|
if (!preWarmedCliSessionId) {
|
|
299
305
|
console.warn('[claude-bridge] CLI pre-warm completed but no session ID captured')
|
|
@@ -738,6 +744,9 @@ export async function callClaudeStreaming(
|
|
|
738
744
|
effortLevel: resolvedEffort ?? getClaudeEffortLevel(),
|
|
739
745
|
cliModelId: keyProfile ? keyProfile.model : resolveClaudeCliModelId(resolvedModel),
|
|
740
746
|
})
|
|
747
|
+
// 6.97.0: what this run used, for the audit row only. --resume decides how its totals can be read.
|
|
748
|
+
const runUsage = new ClaudeRunUsage(!!existingCliSession)
|
|
749
|
+
let auditRowWritten = false
|
|
741
750
|
|
|
742
751
|
// Ultracode is a CLI-only orchestration keyword; history/chat keeps the
|
|
743
752
|
// original user text so the keyword never appears on the lens.
|
|
@@ -927,6 +936,7 @@ export async function callClaudeStreaming(
|
|
|
927
936
|
const inputEstimate = systemPrompt.length + fullQuery.length + contextPrompt.length
|
|
928
937
|
// Key lanes (P11): this turn's settled spend on the key. Keyed turns only.
|
|
929
938
|
const keyUsdFields = keyTurnId ? { usd: Math.round(getSharedKeyMeter().turnSettledUsd(keyTurnId) * 1e6) / 1e6 } : {}
|
|
939
|
+
const realUsage = runUsage.take()
|
|
930
940
|
logTokenAudit({
|
|
931
941
|
source: options?.lightweight ? 'g2-voice' : 'g2-query',
|
|
932
942
|
model: resolvedModel,
|
|
@@ -936,7 +946,11 @@ export async function callClaudeStreaming(
|
|
|
936
946
|
caller: options?.auditCaller ?? (options?.lightweight ? 'voice_query' : 'full_query'),
|
|
937
947
|
...keyAuditFields,
|
|
938
948
|
...keyUsdFields,
|
|
949
|
+
runId: run.runId,
|
|
950
|
+
...auditCliSession(),
|
|
951
|
+
...(realUsage ? { realUsage } : {}),
|
|
939
952
|
})
|
|
953
|
+
auditRowWritten = true
|
|
940
954
|
|
|
941
955
|
finishClaudeRun(run.runId, {
|
|
942
956
|
status: 'completed',
|
|
@@ -963,6 +977,33 @@ export async function callClaudeStreaming(
|
|
|
963
977
|
notifyExchange(sid, query, text)
|
|
964
978
|
}
|
|
965
979
|
|
|
980
|
+
function auditCliSession(): { cliSessionId?: string } {
|
|
981
|
+
const cliSessionId = runUsage.cliSessionId ?? existingCliSession
|
|
982
|
+
return cliSessionId ? { cliSessionId } : {}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* A run can finish on its first result while background subagents are still going; their usage is
|
|
987
|
+
* stated on a later result, after the row above was written. It gets one row of its own under the
|
|
988
|
+
* same run id, so each result counts once and none is lost. Zero chars: it carries only real usage.
|
|
989
|
+
*/
|
|
990
|
+
function auditLateUsage() {
|
|
991
|
+
if (!auditRowWritten) return
|
|
992
|
+
const realUsage = runUsage.take()
|
|
993
|
+
if (!realUsage) return
|
|
994
|
+
logTokenAudit({
|
|
995
|
+
source: options?.lightweight ? 'g2-voice' : 'g2-query',
|
|
996
|
+
model: resolvedModel,
|
|
997
|
+
inputChars: 0,
|
|
998
|
+
outputChars: 0,
|
|
999
|
+
durationMs: Date.now() - startTime,
|
|
1000
|
+
caller: `${options?.auditCaller ?? (options?.lightweight ? 'voice_query' : 'full_query')}:late_result`,
|
|
1001
|
+
...keyAuditFields, runId: run.runId,
|
|
1002
|
+
...auditCliSession(),
|
|
1003
|
+
realUsage,
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
|
|
966
1007
|
async function finalizeError(
|
|
967
1008
|
msg: string,
|
|
968
1009
|
exitCode?: number | null,
|
|
@@ -1120,6 +1161,7 @@ export async function callClaudeStreaming(
|
|
|
1120
1161
|
|
|
1121
1162
|
try {
|
|
1122
1163
|
const event = JSON.parse(trimmed)
|
|
1164
|
+
runUsage.observe(event)
|
|
1123
1165
|
|
|
1124
1166
|
// 6.58.2: the first event that carries the session id (a hook or init line, before any
|
|
1125
1167
|
// answer text) links the run and the job to it. At completion the result's id is
|
|
@@ -1274,6 +1316,11 @@ export async function callClaudeStreaming(
|
|
|
1274
1316
|
keyTurn?.childClosed()
|
|
1275
1317
|
if (steerReaper) clearTimeout(steerReaper)
|
|
1276
1318
|
steerPort?.close()
|
|
1319
|
+
// 6.97.0: nothing more can arrive. A last line with no newline is read here, once, for the audit.
|
|
1320
|
+
if (buffer.trim()) {
|
|
1321
|
+
try { runUsage.observe(JSON.parse(buffer.trim())) } catch { /* not JSON: handled below */ }
|
|
1322
|
+
}
|
|
1323
|
+
auditLateUsage()
|
|
1277
1324
|
if (terminationRequested) return
|
|
1278
1325
|
// Process any remaining buffer
|
|
1279
1326
|
if (buffer.trim()) {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
skipsCosServiceTier,
|
|
18
18
|
} from './codex-extra-args.js'
|
|
19
19
|
import { logTokenAudit } from './token-audit.js'
|
|
20
|
+
import { CodexRunUsage } from './provider-usage.js'
|
|
20
21
|
import { cleanupModelImageInputs, type ModelImageInput } from './model-image-input.js'
|
|
21
22
|
import { buildSystemPrompt, buildLightweightSystemPrompt } from './context-builder.js'
|
|
22
23
|
import {
|
|
@@ -644,6 +645,9 @@ export async function callCodexStreaming(
|
|
|
644
645
|
// the user's own words, bound by the hash of exactly what goes on stdin; the file dies
|
|
645
646
|
// with the child. See lib/turn-request.ts.
|
|
646
647
|
const turnRequest = createTurnRequestBinding({ userText: query, prompt, turnId: options?.turnId })
|
|
648
|
+
// 6.97.0: what this run used, for the audit row only. Begins before the spawn, which writes to the rollout.
|
|
649
|
+
const runUsage = new CodexRunUsage(keyProfile ? keyProfile.model : !skipsCosModel(extra) ? resolvedCodexModel?.id : undefined)
|
|
650
|
+
runUsage.begin(engineSession?.codexThreadId)
|
|
647
651
|
applyTurnRequestEnv(env, turnRequest)
|
|
648
652
|
|
|
649
653
|
const proc = withKeyTurnRevokedOnThrow(keyTurn, () => spawnWithTurnRequest(turnRequest, () => spawn(resolvedCodex.path, args, {
|
|
@@ -811,6 +815,7 @@ export async function callCodexStreaming(
|
|
|
811
815
|
const totalMs = Date.now() - startTime
|
|
812
816
|
// Key lanes (P11): this turn's settled spend on the key. Keyed turns only.
|
|
813
817
|
const keyUsdFields = keyTurnId ? { usd: Math.round(getSharedKeyMeter().turnSettledUsd(keyTurnId) * 1e6) / 1e6 } : {}
|
|
818
|
+
const realUsage = runUsage.finish(codexThreadId, engineSession?.codexThreadId)
|
|
814
819
|
logTokenAudit({
|
|
815
820
|
source: options?.lightweight ? 'g2-voice' : 'g2-query',
|
|
816
821
|
model,
|
|
@@ -820,6 +825,9 @@ export async function callCodexStreaming(
|
|
|
820
825
|
caller: options?.auditCaller ?? (options?.lightweight ? 'voice_query' : 'full_query'),
|
|
821
826
|
...keyAuditFields,
|
|
822
827
|
...keyUsdFields,
|
|
828
|
+
runId: run.runId,
|
|
829
|
+
...(codexThreadId ? { cliSessionId: codexThreadId } : {}),
|
|
830
|
+
...(realUsage ? { realUsage } : {}),
|
|
823
831
|
})
|
|
824
832
|
saveEngineSessionBestEffort()
|
|
825
833
|
|
|
@@ -948,6 +956,7 @@ export async function callCodexStreaming(
|
|
|
948
956
|
|
|
949
957
|
let nativeSessionAnnounced = false
|
|
950
958
|
function handleEvent(event: any) {
|
|
959
|
+
runUsage.observe(event)
|
|
951
960
|
const nextThreadId = extractCodexThreadId(event)
|
|
952
961
|
if (nextThreadId && nextThreadId !== codexThreadId) {
|
|
953
962
|
codexThreadId = nextThreadId
|
|
@@ -1032,10 +1041,11 @@ export async function callCodexStreaming(
|
|
|
1032
1041
|
|
|
1033
1042
|
proc.on('close', (code) => {
|
|
1034
1043
|
keyTurn?.childClosed()
|
|
1035
|
-
if (terminationRequested) return
|
|
1044
|
+
if (terminationRequested) { runUsage.settle(); return }
|
|
1036
1045
|
if (buffer.trim()) {
|
|
1037
1046
|
try { handleEvent(JSON.parse(buffer.trim())) } catch { /* ignore */ }
|
|
1038
1047
|
}
|
|
1048
|
+
runUsage.settle()
|
|
1039
1049
|
if (finalized) return
|
|
1040
1050
|
if (fullText) {
|
|
1041
1051
|
void finalize(fullText)
|