@gotcos/glasses-server 6.79.1 → 6.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -0
- package/CHANGELOG.md +98 -0
- package/README.md +20 -1
- package/managed-runtime-contract.json +6 -1
- package/package.json +9 -1
- package/server/index.ts +11 -0
- package/server/lib/agent-session-search.ts +8 -5
- package/server/lib/agent-session-store.ts +45 -4
- package/server/lib/archive.ts +2 -1
- package/server/lib/attached-provider-adapter.ts +11 -0
- package/server/lib/claude-bridge.ts +112 -8
- package/server/lib/claude-settings-guard.ts +94 -0
- package/server/lib/claude-steer.ts +27 -0
- package/server/lib/codex-bridge.ts +205 -32
- package/server/lib/codex-engine-sessions.ts +27 -6
- package/server/lib/codex-homes.ts +134 -0
- package/server/lib/codex-live-queue.ts +14 -0
- package/server/lib/codex-profile-home.ts +452 -0
- package/server/lib/codex-queue-control.ts +17 -1
- package/server/lib/codex-subagent-activity.ts +22 -0
- package/server/lib/conversation.ts +92 -15
- package/server/lib/dictation-clean.ts +13 -6
- package/server/lib/dictation-fillers.ts +17 -10
- package/server/lib/dictation-polish-need.ts +48 -0
- package/server/lib/dictation-prewarm.ts +209 -0
- package/server/lib/direct-bridge.ts +853 -0
- package/server/lib/direct-sse.ts +262 -0
- package/server/lib/fork-thread.ts +11 -0
- package/server/lib/key-answer-fields.ts +49 -0
- package/server/lib/key-catalog.ts +301 -0
- package/server/lib/key-lanes-pricing.ts +12 -0
- package/server/lib/key-lanes-wiring.ts +346 -0
- package/server/lib/key-lanes.ts +145 -0
- package/server/lib/key-ledger.ts +123 -0
- package/server/lib/key-notices.ts +486 -0
- package/server/lib/key-profile-rejects.ts +25 -0
- package/server/lib/key-profile-routing.ts +173 -0
- package/server/lib/key-profiles.ts +182 -0
- package/server/lib/key-relay-contract.ts +262 -0
- package/server/lib/key-relay-errors.ts +211 -0
- package/server/lib/key-relay-meter.ts +303 -0
- package/server/lib/key-relay.ts +1018 -0
- package/server/lib/key-secrets-guard.ts +52 -0
- package/server/lib/key-store.ts +228 -0
- package/server/lib/keyed-sessions.ts +154 -0
- package/server/lib/lane-b-env.ts +169 -0
- package/server/lib/live-cues-v2-settings.ts +51 -13
- package/server/lib/live-cues-v2.ts +147 -19
- package/server/lib/model-router.ts +96 -7
- package/server/lib/morning-brief-config.ts +3 -0
- package/server/lib/native-head.ts +13 -1
- package/server/lib/occupancy-probes.ts +4 -0
- package/server/lib/ollama-run-ledger.ts +96 -13
- package/server/lib/prompt-edit.ts +2 -0
- package/server/lib/query-job-coordinator.ts +2 -1
- package/server/lib/query-job-runtime.ts +67 -15
- package/server/lib/query-job-store.ts +7 -1
- package/server/lib/query-job-types.ts +15 -1
- package/server/lib/session-hook-events.ts +28 -0
- package/server/lib/session-hook-ledger.ts +10 -0
- package/server/lib/thread-occupancy.ts +22 -1
- package/server/lib/token-audit.ts +18 -4
- package/server/lib/work-handoff-requests.ts +3 -0
- package/server/routes/agent-session-bindings.ts +15 -0
- package/server/routes/agent-sessions.ts +17 -5
- package/server/routes/handoffs.ts +22 -1
- package/server/routes/health.ts +5 -0
- package/server/routes/key-lanes.ts +442 -0
- package/server/routes/live-cues.ts +39 -3
- package/server/routes/openai-compat.ts +28 -3
- package/server/routes/prompt-drafts.ts +75 -8
- package/server/routes/query.ts +20 -6
- package/shared/key-preference.ts +57 -0
- package/shared/model-preference.ts +20 -2
package/.env.example
CHANGED
|
@@ -417,3 +417,12 @@ COS_CONTINUE_FULL_PERMISSIONS=1
|
|
|
417
417
|
# COS_PIN_BOOT_REAP=1 # free a thread a crashed run held, 31 min after the restart and once its child is gone (default on; 0 = the 24 h reap)
|
|
418
418
|
# COS_QUEUE_RESEND=1 # Send again on a refused or expired queued turn (default on)
|
|
419
419
|
# COS_SESSION_STEER=0 # steer a busy Claude session through its peer inbox (default off)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ── Answer with your own key (6.80.0, opt in) ──────────────────────────────────
|
|
423
|
+
# Off by default. With this unset, nothing about key lanes runs until COS Control turns them on
|
|
424
|
+
# from its Settings row (that writes data/key-lanes.json). 1 turns them on from the environment;
|
|
425
|
+
# 0 forces them off and wins over Control. Keys are saved only through COS Control on this Mac,
|
|
426
|
+
# in data/key-secrets/ (0700). Your key is saved on this Mac: apps and agents that run on this
|
|
427
|
+
# Mac as you can read it, so your provider's own spending limit is the real stop.
|
|
428
|
+
# COS_KEY_LANES=0
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
+
## 6.80.0 - Answer with your own key (opt in, off by default)
|
|
2
|
+
|
|
3
|
+
You can now give COS your own provider key (OpenRouter, Anthropic, OpenAI, or a compatible endpoint) and pick a key profile for a thread, so that thread answers on the key instead of your Claude or ChatGPT plan. **This is opt in. Without a key, nothing changes:** with `COS_KEY_LANES` unset or `0`, or with lanes on but no key profile saved, the server answers, spawns and writes exactly as 6.79.1 did. Turning it on and saving keys happens in COS Control on this Mac.
|
|
4
|
+
|
|
5
|
+
- **Client matrix.** Server only in this release. COS Control and the companion need their key-lane builds to show the setting, the picker entries and the cost label; older apps never call the new routes and see nothing new. `COS_KEY_LANES=0` (now forwarded to a managed server) forces everything off whatever Control saved.
|
|
6
|
+
- **How a keyed answer runs.** The real key stays inside the server. A keyed Claude or Codex turn talks to a loopback relay (127.0.0.1, an ephemeral port, opened only by the first keyed turn) with a one-turn token; the relay checks it, meters the turn against the key's daily cap, forwards with the key and settles what the answer cost. A direct profile (any OpenRouter model, or a compatible endpoint) answers in process through the same meter and breakers. A keyed turn never falls back to your plan: if the key, the relay or the cap says no, the lens says why and your plan is not used.
|
|
7
|
+
- **What the routes show.** While lanes are on: `/api/key-lanes/*` (status, keys, profiles, catalog, remove all, disable), and `details.keyLanes` on `/api/health`. Status is readable from the phone with the pairing token (cap, today's spend, the UTC reset time as `resetsAt`, never the key); every write stays Mac only. With a key profile saved: `keyProfiles` on `/api/models`, and a keyed answer's `done` event carries `lane`, `credential` (kind, key ref, profile id), `profile` and `usd`. The one route present while lanes are off is `POST /api/key-lanes/enable`, for COS Control on this Mac (token, loopback, no browser).
|
|
8
|
+
- **Errors say what happened.** When the key itself stops a turn (daily cap used, key rejected, out of credit, the model gone, or the key paused after three failures), the lens, the job record and COS Control show the matching one-line reason instead of the CLI's raw error. A turn refused before it starts reads the same way.
|
|
9
|
+
- **Recovered answers keep their cost.** A durable job answered on a key keeps `lane`, `credential`, `profile` and `usd` on its record and through a restart, so an answer the app recovers later still shows what it cost. A job on your plan writes exactly the record it did before.
|
|
10
|
+
- **Keyed Codex threads in Sessions.** With lanes on, a Codex thread that ran on a key is listed, named, searchable and shows its subagents like any other. With lanes off, only the usual Codex home is read.
|
|
11
|
+
- **Where the key lives, plainly.** Keys are saved in `data/key-secrets/provider-keys.json` (0600, in a 0700 folder of their own). While key lanes are on and a key is saved, every Claude run that can use tools is told it may not Read, Edit, Write or `cat` that folder (checked against the real Claude Code 2.1.304: both a Read and a Bash `cat` come back denied). Codex has no way to exclude a folder from what it can read. So the honest statement, which COS Control also shows: Your key is saved on this Mac. Apps and agents that run on this Mac as you can read it. Your provider's spending limit is the real stop. OpenRouter keys still need a credit limit set; Anthropic and OpenAI keys have no provider-side daily backstop, so the cap here is the only one.
|
|
12
|
+
- **The cap is checked mid-reply, and is not a hard stop.** A request reserves its own max_tokens within what the key has left today, and a reply whose running cost would pass the cap is cut and settled at what the provider billed. The cap can be exceeded by the last reply in progress: the provider bills what it already generated, so the overshoot is bounded by that one response's max_tokens (a real-key cut late in a 14k-token reply went $0.003 past $0.0065 of headroom). Your provider limit is the hard stop. A spend that a crash left open still counts after a restart.
|
|
13
|
+
- **More you can do from COS Control.** Reset a key paused after three failures (`POST /api/key-lanes/breaker/reset`); turning a profile off, or changing its key or model, stops its live turn at once, and a new key or model clears the profile's pause. The one-turn "run this one on your key" offer (`keyTurnOnly`) answers that turn on the key without moving the thread onto it.
|
|
14
|
+
- **Tighter edges.** Custom endpoints must be https unless they are on this Mac. Claude managed settings and `CLAUDE_CONFIG_DIR` are checked, and OAuth and Bedrock token names are never passed to a keyed run; a Codex project config that sets a provider refuses a keyed Codex turn. Rate limits, provider errors, refusals and an expired or stopped key turn each read as their own one-line reason.
|
|
15
|
+
- **Fixed from the real-key canaries.** A keyed Claude run now forces agent teams off even when a project settings file turns them on (checked with the real CLI). Parallel subagents on one keyed turn wait for a free slot (16 at once, then a queue) instead of failing, and a turn that still cannot get one reads "Too many steps at once. Your plan was not used." A Codex failure now costs at most 3 attempts on the key, and the key turn ends the moment the answer does. Cached prompt tokens are priced at their real rates, so a reply that fits under the cap is no longer cut, and a cut reply is reconciled to its real cost. Hook events of a keyed Claude session or Codex thread now carry their key profile, also on the `session-hook-events.jsonl` rows (resolved from the keyed-session record the bridge wrote; rows with lanes off are unchanged). Claude Code's reachability probe gets an empty answer instead of a 404.
|
|
16
|
+
- **Safety rails.** Daily cap per key, shared by every profile, turn and lane on it; the cap Control saved wins over any later edit of the key file. Three rejected-key, out-of-credit or gone-model failures pause that key or profile for an hour. Error bodies are scrubbed of the key. A session that ran on a key is never resumed, handed off, continued or forked on your plan (Continue and Fork answer the same 409 `keyed_session` body as handoffs). A keyed Codex thread keeps its own resume slot, so switching lanes never loses the other lane's thread. Scheduled briefs, task dispatch and Work drafts never run on a key.
|
|
17
|
+
- **Known limit (not from key lanes).** When a turn starts background subagents, the CLI can emit a first `result` before they finish ("Eight of the 12 have reported...") and a full one later. COS ends the turn on the first `result`, so the lens gets the partial answer. This is the same on your plan at 6.79.1 (a0035a8) and 6.79.2 (ba4fc66), in normal and steer mode (`claude-bridge-two-results.test.ts` passes on both unchanged), and a keyed turn runs with the same arguments as a plan turn (no stream-json input), so it is recorded here and not changed in this candidate.
|
|
18
|
+
- **Checks.** Full suite 8,399 passed, 0 failed, 3 skipped (553 files), every test that passed at ba4fc66 (6.79.2, 7,962) still passing by name; `tsc --noEmit` clean. Combined gate `server/scripts/key-lanes-mutation-gate.mjs` (not packaged): P1 48, P2 36, P3 34, P4 38, P5 28, P6 30, P8 22 and 104 integration mutants, every one killed by a named test against a green baseline. The off-golden re-recorded at ba4fc66 (6.79.2, a clean tree with no key code) compares each CLI's normalized stdin; it replays with an empty diff at `COS_KEY_LANES=0`; unset, the only difference is the Control enable door; at `=1` with no profile, exactly that door plus `details.keyLanes` and the status route. Journeys (`key-lanes-journeys.ts`, booting `server/index.ts` against a loopback mock): 24 of 24. `npm pack --dry-run` ships no test, gate or fixture.
|
|
19
|
+
|
|
20
|
+
## 6.79.2 - Live cue limits from COS Control: the bar, cues per meeting, cooldown and a daily writer ceiling, applied without a restart
|
|
21
|
+
|
|
22
|
+
Miles, 2026-10-19: cap live-cue spend from COS Control. On 10/19, live cues ran 126 Sonnet writer calls (about 310K writer tokens) across 7 meetings.
|
|
23
|
+
|
|
24
|
+
- **Client matrix.**
|
|
25
|
+
- COS Control 0.5.328 (or later) reads and sets the limits.
|
|
26
|
+
- Control 0.5.327 and older keep working byte for byte: GET and POST without a version answer exactly the 6.79.1 body.
|
|
27
|
+
- No companion change is required. The lens shows a meeting stopped by the daily ceiling as `budget_exhausted`, a reason it already knows.
|
|
28
|
+
- **Four limits, stored in `live-cues-v2-settings.json`** (atomic write, mode 0600, the file that holds the on/off switch). They apply from each meeting's next evaluation, with no restart.
|
|
29
|
+
- `gate`: the gate's bar, 0.5 to 0.95. Default 0.8.
|
|
30
|
+
- `maxCuesPerMeeting`: 1 to 25, default 25. The writer cap per meeting stays twice this.
|
|
31
|
+
- `cooldownSeconds`: whole seconds, 20 to 900. Default 45.
|
|
32
|
+
- `dailyWriterCalls`: 1 to 500, default 150. **This is a new ceiling**: writer calls per local calendar day across all meetings. The weekly ceiling is unchanged, so this caps today at 150 writer calls a day where before only the weekly 750 applied.
|
|
33
|
+
- A value saved out of bounds (a hand edit) reads as unset, never as a looser limit.
|
|
34
|
+
- A file holding only the switch is still written as schema 1, so a 6.79.1 server reads it as before. Any saved limit makes it schema 2, which 6.79.1 also reads (it looks only at `enabled`).
|
|
35
|
+
- **Env wins and locks.** `COS_LIVE_CUES_V2_GATE` (its acceptance unchanged, 0.3 to 0.99), `COS_LIVE_CUES_V2_MAX_PER_MEETING` (new, whole 1 to 25), `COS_LIVE_CUES_V2_COOLDOWN_S` and `COS_LIVE_CUES_V2_DAILY_WRITER_CALLS` (new, whole 1 to 500).
|
|
36
|
+
- A valid env value is reported `source: "env", locked: true`.
|
|
37
|
+
- A POST naming a locked limit is 409 `env_locked` and saves nothing.
|
|
38
|
+
- An env value outside its range does not lock: the saved setting or the default applies, with one log line per value.
|
|
39
|
+
- Both new variables are in `managed-runtime-contract.json` `optionalEnvironment`.
|
|
40
|
+
- **Engine.**
|
|
41
|
+
- The gate, cue cap, writer cap and cooldown are read from the resolver on every feed and evaluation.
|
|
42
|
+
- A meeting already past a lowered cap stops at its next feed (skip `meeting_cap`, no gate call), and resumes if the cap is raised.
|
|
43
|
+
- The daily ceiling is checked before the gate (skip `daily_cap`, so no Jev spend either).
|
|
44
|
+
- Both the cap and the daily ceiling are checked again just before the writer is charged, so a limit lowered mid-evaluation applies at once (outcome `meeting_cap` or `daily_cap`).
|
|
45
|
+
- On the wire the daily ceiling reads as `budget_exhausted`: the shared stop-reason union, pinned with the companion's copy, has no `daily_cap`.
|
|
46
|
+
- Cue generation, prompts and the judge are unchanged.
|
|
47
|
+
- **Today's usage.** The durable state file gains `day` and `today` `{writerCalls, writerTokensIn, writerTokensOut, cues, jevTokens}` on the local calendar day of the zone it already stores.
|
|
48
|
+
- Writer calls count when charged, cues when shown, and tokens once per evaluation from the numbers it traced. `jevTokens` is the gate plus the judge, from the decision provider that ran them: Jev, or SubQ when SubQ runs cues.
|
|
49
|
+
- A future-dated, malformed or unreadable day reads its writer calls as spent, like the week: it fails toward not spending.
|
|
50
|
+
- A pre-6.79.2 file reads as zero for today.
|
|
51
|
+
- **Routes.** `GET /api/live-cues/v2/settings?v=2` and `POST {"v": 2, ...}` answer the v1 body plus `v`, `limits`, `writerCallsPerMeeting` and `today`.
|
|
52
|
+
- Each limit is reported as `{value, source, locked, min, max, default}`.
|
|
53
|
+
- POST v2 accepts any subset of `enabled`, `gate`, `maxCuesPerMeeting`, `cooldownSeconds` and `dailyWriterCalls`. Every field is checked before one write, so a refused request changes nothing.
|
|
54
|
+
- 400 `invalid_request` with `reason`: `out_of_bounds` (with `field`, `min`, `max`, `integer`), `unknown_field`, `invalid_enabled`, `empty` or `unsupported_version`.
|
|
55
|
+
- 409 `env_locked` (with `fields`), or `env_forced` when turning v2 off while env forces it on.
|
|
56
|
+
- An older server ignores `?v=2` and answers without `v`: that is Control's feature detection.
|
|
57
|
+
- **Dictation filler pass (from `320ecb7`, which shipped after 6.79.1 was published from `a0035a8`).**
|
|
58
|
+
- **Titles and abbreviations.** A Titlecase token right after a title or abbreviation (Mr., Mrs., Ms., Dr., Prof., St., Jr., Sr., vs., etc., e.g., i.e., a.m., p.m., U.S., an initial) is a name and is kept: "We met Mr. Ah and Ms. Er yesterday" and "Dr. Er said yes" stay as said.
|
|
59
|
+
- A lowercase filler there still goes, with no capital hand-off: "We met Mr. uh Jones" becomes "We met Mr. Jones", and "at 5 p.m. uh we meet" becomes "at 5 p.m. we meet". In 6.79.1 every token after a title was kept, so "Mr. uh Jones" kept the "uh", and Jr. and Sr. were not abbreviations.
|
|
60
|
+
- **Sentence ends.** An ellipsis does not end a sentence. The capital passes on only when the removed filler was itself a sentence's first word. The abbreviation check that became unreachable once the rule above runs first is removed.
|
|
61
|
+
- Dictation mutation gate `server/scripts/dictation-polish-mutation-gate.mjs`: 55 of 55 against a 133-test baseline.
|
|
62
|
+
- **Faster dictation polish: start the polish process at key press.**
|
|
63
|
+
- **Why.** Miles, 2026-10-19: polish takes 1.8 to 4.1 s with Haiku (median about 2.4 s). A trivial `claude -p` with the polish args takes 1.6 to 1.8 s, so most of the wait is the CLI starting, not the edit.
|
|
64
|
+
- **Canary on his Mac (Haiku, 9 real calls).** Cold: 1786, 1894, 1886, 1968 ms. A process started 3 to 5 s before the text answered 962 to 1305 ms after the text arrived; one started 1 s before took 1501 ms, because the CLI had not finished starting.
|
|
65
|
+
- **`POST /api/dictation/polish/prewarm`** `{model, surface?}` answers `{token, expiresMs}`. It spawns the exact polish process now (same args: no tools, MCP, hooks or session file, its own empty workspace) with `--input-format stream-json`, waiting for one message.
|
|
66
|
+
- Refusals are 409 `{error: "prewarm_unavailable", reason}`, with reason `disabled`, `model` (Luna cannot be warmed), `breaker_open`, `daily_cap` or `cli`.
|
|
67
|
+
- `DELETE /api/dictation/polish/prewarm/<token>` discards one.
|
|
68
|
+
- **`/api/dictation/finalize` takes an optional `prewarmToken`.**
|
|
69
|
+
- A live token for the same model gets the one message, then its stdin is closed and it exits: no history ever carries over.
|
|
70
|
+
- A process that died before the message falls back to today's cold spawn, the same single call. A failure after the message was sent is a real `failed`, never retried cold.
|
|
71
|
+
- With a token, the answer adds `prewarm`: `used`, `missed` (unknown, expired, evicted, wrong model or dead) or `discarded` (the model was not called, so the process was killed).
|
|
72
|
+
- A malformed token is 400 `invalid_prewarm_token`.
|
|
73
|
+
- **Bounds.**
|
|
74
|
+
- At most 2 warm processes; a third evicts the oldest.
|
|
75
|
+
- Each is killed 150 s after it starts if unused.
|
|
76
|
+
- Every live one is signalled when the server exits, claimed or not.
|
|
77
|
+
- The daily polish budget is spent only when one is used; a warm process makes no model call until its message.
|
|
78
|
+
- `COS_DICTATION_PREWARM=0` turns it off.
|
|
79
|
+
- **Clients.** No companion change. Control needs a change to call it, specified in the COS workspace (`wk43_2026/dictation_polish/PREWARM_CONTROL_SPEC.md`). Control 0.5.328's polish bridge ignores the new answer keys.
|
|
80
|
+
- **Skip the model when the text is clean (off by default).**
|
|
81
|
+
- **The rule.** With `COS_DICTATION_POLISH_SKIP_CLEAN=1`, finalize returns the filler-passed text with `polishSkipped: "clean"` unless the text has any of these:
|
|
82
|
+
- a backtrack marker
|
|
83
|
+
- a repeated word or restarted clause
|
|
84
|
+
- a filler-like ("like", "you know", "kind of", or a leading "So", "Well" or "Okay")
|
|
85
|
+
- a filler sound
|
|
86
|
+
- spoken formatting or punctuation
|
|
87
|
+
- no opening capital or no closing punctuation
|
|
88
|
+
- more than 40 words
|
|
89
|
+
|
|
90
|
+
The rule is `polishNeed` in `server/lib/dictation-polish-need.ts`, with a table test.
|
|
91
|
+
- **Eval on the 45 synthetic cases.**
|
|
92
|
+
- As dictated (lowercase, unpunctuated), all 45 still reach the model, so nothing changes.
|
|
93
|
+
- Capitalized with a closing period, as Mac dictation arrives, 15 would skip. 3 of those lose a word fix that v2 makes on both models: mis-heard names ("tablo" for Tableau and "jewel three sixty" for Jewel360, "preeya" for Priya and "hub spot" for HubSpot, "p o s" for POS). No rule can see those.
|
|
94
|
+
- It therefore stays off.
|
|
95
|
+
- **Managed runtime.** `COS_DICTATION_PREWARM` and `COS_DICTATION_POLISH_SKIP_CLEAN` are in `managed-runtime-contract.json` `optionalEnvironment`.
|
|
96
|
+
- **Dictation mutation gate** (`server/scripts/dictation-polish-mutation-gate.mjs`, not packaged): 92 of 92 against a 199-test baseline in 7 files. 37 of them are new for the prewarm and the skip rule.
|
|
97
|
+
- **Checks.** Full suite 7,896 passed, 3 skipped (510 files); `tsc --noEmit` clean. Mutation gate `server/scripts/live-cues-verdicts-mutation-gate.mjs` (not packaged): 179 of 179 guard mutants killed by named tests against a proven green baseline (149 tests in 8 files), 48 of them new for the limits. The existing pins (prompts, contract hash, calibrated defaults, the v1 settings body and its schema-1 file) are unchanged.
|
|
98
|
+
|
|
1
99
|
## 6.79.1 - Dictation polish: one budget per caller, a reason when it does not polish, no more um and uh
|
|
2
100
|
|
|
3
101
|
Miles, 2026-10-19: AI polish for Mac dictation failed on every dictation from 11:39 CDT (`failed:not_polished` in about 60 ms). Every autoclean shared one daily cap (`COS_DICTATION_AUTOCLEAN_MAX_PER_DAY`). The glasses' live meeting lines (surface `meeting`, Sonnet, one call per 10-second chunk while the phone's Moonshine lane is up) spent it by late morning. Nothing in the answer said why.
|
package/README.md
CHANGED
|
@@ -787,10 +787,19 @@ VOICE) follow the same settings. Contract and measurements:
|
|
|
787
787
|
- Messages and prompt drafts drop standalone um, uh, er, ah, hmm, mm and mhm
|
|
788
788
|
before the model and when it is skipped. Meeting lines are left as heard.
|
|
789
789
|
It keeps ALL-CAPS tokens, a Titlecase token mid-sentence (a name) unless a
|
|
790
|
-
comma follows it,
|
|
790
|
+
comma follows it, a Titlecase token after a title or abbreviation (Mr. Ah,
|
|
791
|
+
Dr. Er) and quoted mentions.
|
|
791
792
|
`COS_DICTATION_FILLER_PASS=0` turns the pass off.
|
|
792
793
|
- `COS_DICTATION_POLISH_PROMPT=v1` restores the 6.79.0 polish prompt (default
|
|
793
794
|
`v2`).
|
|
795
|
+
- 6.79.2: `POST /api/dictation/polish/prewarm` `{model}` starts the polish
|
|
796
|
+
process at key press and answers `{token, expiresMs}`. Pass the token as
|
|
797
|
+
`prewarmToken` to `/api/dictation/finalize` and the polish skips the CLI's
|
|
798
|
+
start-up (about 0.9 s on a Mac). At most 2 warm processes, 150 s each, one
|
|
799
|
+
message each. `COS_DICTATION_PREWARM=0` turns it off.
|
|
800
|
+
- 6.79.2: `COS_DICTATION_POLISH_SKIP_CLEAN=1` returns finished, clean text
|
|
801
|
+
without the model (`polishSkipped: "clean"`). Off by default: it cannot see
|
|
802
|
+
a mis-heard name.
|
|
794
803
|
|
|
795
804
|
### Parakeet (FluidAudio CoreML, offline)
|
|
796
805
|
|
|
@@ -1036,6 +1045,16 @@ usable on Fast and reports the downgrade truthfully. Set
|
|
|
1036
1045
|
`COS_BATCH_LARGE_V3=0` to explicitly use turbo. Interactive HQ uses beam 2 by
|
|
1037
1046
|
default (`COS_HQ_BEAM_INTERACTIVE`); meeting batch remains beam 5.
|
|
1038
1047
|
|
|
1048
|
+
## Answer with your own key (6.80.0, opt in)
|
|
1049
|
+
|
|
1050
|
+
Off by default, and without a key nothing changes. Turn it on in COS Control (Settings, "Answer with your own key"), or set `COS_KEY_LANES=1`; `COS_KEY_LANES=0` forces it off.
|
|
1051
|
+
|
|
1052
|
+
- **What it does.** You save a provider key (OpenRouter, Anthropic, OpenAI or a compatible endpoint) in COS Control and make a key profile: which harness answers (Claude Code, Codex, or a direct call), which model, and a daily cap. A thread you put on that profile answers on the key instead of your plan. It never falls back to your plan: when the key, the cap or the provider says no, the lens says why.
|
|
1053
|
+
- **Where the key lives.** In `data/key-secrets/provider-keys.json` (0600, in a 0700 folder), used only by the server's loopback relay, which gives each keyed turn a one-turn token instead of the key. Every Claude spawn that can use tools gets deny rules for that folder. Codex has no sandbox read exclusion. Plainly: your key is saved on this Mac, apps and agents that run on this Mac as you can read it, and your provider's spending limit is the real stop. OpenRouter keys must have a credit limit set; Anthropic and OpenAI keys have no provider-side daily backstop, so the cap here is the only one.
|
|
1054
|
+
- **Spend.** A daily cap per key (UTC day), shared by every profile on it. A reply that would pass the cap is cut and the lens shows the cap line. The cap can be exceeded by the last reply in progress (by at most that reply's max_tokens); your provider limit is the hard stop. `GET /api/key-lanes/status` shows cap, today's spend and the reset time.
|
|
1055
|
+
- **Routes** (all 404 while off, except the enable door COS Control uses): `/api/key-lanes/status`, `keys`, `profiles`, `catalog`, `breaker/reset`, `remove-all`, `enable`, `disable`. Writes answer only COS Control on this Mac (token, loopback, no browser).
|
|
1056
|
+
- **Before you downgrade** below 6.80, remove your keys in COS Control. An older server cannot see them and would answer a keyed thread on your plan.
|
|
1057
|
+
|
|
1039
1058
|
## Run from source
|
|
1040
1059
|
|
|
1041
1060
|
```bash
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"COS_LIVE_CUES_V2_COOLDOWN_S",
|
|
34
34
|
"COS_LIVE_CUES_V2_JUDGE_KEEP",
|
|
35
35
|
"COS_LIVE_CUES_V2_REPEAT_OVERLAP",
|
|
36
|
+
"COS_LIVE_CUES_V2_MAX_PER_MEETING",
|
|
37
|
+
"COS_LIVE_CUES_V2_DAILY_WRITER_CALLS",
|
|
36
38
|
"COS_LIVE_CUES_JEV_DAILY_TOKENS",
|
|
37
39
|
"COS_LIVE_CUES_SUBQ_DAILY_TOKENS",
|
|
38
40
|
"COS_DECISION_PROVIDER_CUES",
|
|
@@ -72,12 +74,15 @@
|
|
|
72
74
|
"COS_PIN_BOOT_REAP",
|
|
73
75
|
"COS_QUEUE_RESEND",
|
|
74
76
|
"COS_SESSION_STEER",
|
|
77
|
+
"COS_KEY_LANES",
|
|
75
78
|
"COS_DICTATION_AUTOCLEAN_MAX_PER_DAY",
|
|
76
79
|
"COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_MESSAGE",
|
|
77
80
|
"COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_MEETING",
|
|
78
81
|
"COS_DICTATION_AUTOCLEAN_MAX_PER_DAY_PROMPT_DRAFT",
|
|
79
82
|
"COS_DICTATION_POLISH_PROMPT",
|
|
80
|
-
"COS_DICTATION_FILLER_PASS"
|
|
83
|
+
"COS_DICTATION_FILLER_PASS",
|
|
84
|
+
"COS_DICTATION_PREWARM",
|
|
85
|
+
"COS_DICTATION_POLISH_SKIP_CLEAN"
|
|
81
86
|
],
|
|
82
87
|
"maintenance": {
|
|
83
88
|
"scope": "cross_boot",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.80.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": {
|
|
@@ -73,6 +73,14 @@
|
|
|
73
73
|
"!server/scripts/transcript-cleanup-mutation-gate.mjs",
|
|
74
74
|
"!server/scripts/live-cues-v2-notes-mutation-gate.mjs",
|
|
75
75
|
"!server/scripts/live-cues-verdicts-mutation-gate.mjs",
|
|
76
|
+
"!server/scripts/key-lanes-p1-mutation-gate.mjs",
|
|
77
|
+
"!server/scripts/key-lanes-p2-mutation-gate.mjs",
|
|
78
|
+
"!server/scripts/key-lanes-p3-mutation-gate.mjs",
|
|
79
|
+
"!server/scripts/key-lanes-p4-mutation-gate.mjs",
|
|
80
|
+
"!server/scripts/key-lanes-p5-mutation-gate.mjs",
|
|
81
|
+
"!server/scripts/key-lanes-p6-mutation-gate.mjs",
|
|
82
|
+
"!server/scripts/key-lanes-p8-mutation-gate.mjs",
|
|
83
|
+
"!server/scripts/key-lanes-mutation-gate.mjs",
|
|
76
84
|
"!server/scripts/dictation-polish-eval.ts",
|
|
77
85
|
"!server/scripts/dictation-polish-mutation-gate.mjs",
|
|
78
86
|
"!server/lib/meeting-engine/__fixtures__/**",
|
package/server/index.ts
CHANGED
|
@@ -79,6 +79,8 @@ import { createTranscriptCleanupRouter } from './routes/transcript-cleanup.js'
|
|
|
79
79
|
import { meetingsRouter } from './routes/meetings.js'
|
|
80
80
|
import { openaiCompatRouter } from './routes/openai-compat.js'
|
|
81
81
|
import { openaiKeyRouter } from './routes/openai-key.js'
|
|
82
|
+
import { createKeyLanesBodyParser, createKeyLanesRouter } from './routes/key-lanes.js'
|
|
83
|
+
import { wireKeyLanes } from './lib/key-lanes-wiring.js'
|
|
82
84
|
import { firefliesKeyRouter } from './routes/fireflies-key.js'
|
|
83
85
|
import { meetingImportRouter } from './routes/meeting-import.js'
|
|
84
86
|
import { startMeetingImportScheduler, stopMeetingImportScheduler } from './lib/meeting-import.js'
|
|
@@ -478,6 +480,10 @@ app.use('/api', (req, res, next) => {
|
|
|
478
480
|
}
|
|
479
481
|
})
|
|
480
482
|
|
|
483
|
+
// Key lanes (plan v4 3.1): parses its own small body ahead of the 10 MB parser, and only for a
|
|
484
|
+
// request the key-lanes router will answer. Every other request passes through untouched.
|
|
485
|
+
app.use('/api/key-lanes', ...createKeyLanesBodyParser({ apiToken: () => API_TOKEN }))
|
|
486
|
+
|
|
481
487
|
// Authenticate before parsing large upload bodies. The 16 MB allowance stays
|
|
482
488
|
// scoped to /api/media; every other route retains the 10 MB ceiling.
|
|
483
489
|
app.use('/api/media/file', mediaBinaryBodyParser)
|
|
@@ -1165,6 +1171,11 @@ app.use('/api', memoryRouter)
|
|
|
1165
1171
|
app.use('/api', threadsRouter)
|
|
1166
1172
|
app.use('/api', skillsRouter)
|
|
1167
1173
|
app.use('/api', openaiKeyRouter)
|
|
1174
|
+
// Key lanes (plan v4 3.1). Off (the default): every /api/key-lanes path falls through to the
|
|
1175
|
+
// usual 404, except an authenticated loopback POST /api/key-lanes/enable from COS Control.
|
|
1176
|
+
app.use('/api', createKeyLanesRouter({ apiToken: () => API_TOKEN }))
|
|
1177
|
+
// P11: fill the key-lane seams (map writes only; the relay's status seams only while enabled).
|
|
1178
|
+
wireKeyLanes()
|
|
1168
1179
|
// Fireflies: the key, and importing meetings this Mac did not record (6.47.0).
|
|
1169
1180
|
app.use('/api', firefliesKeyRouter)
|
|
1170
1181
|
app.use('/api', meetingImportRouter)
|
|
@@ -40,8 +40,9 @@ import {
|
|
|
40
40
|
codexUserText,
|
|
41
41
|
listCodexJsonlFiles,
|
|
42
42
|
loadClaudeStarredIds,
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
codexSessionRootsOf,
|
|
44
|
+
loadCodexPinnedIdsAll,
|
|
45
|
+
loadCodexThreadNamesAll,
|
|
45
46
|
loadCursorComposerNames,
|
|
46
47
|
loadCursorPinnedIds,
|
|
47
48
|
parseJsonLine,
|
|
@@ -314,10 +315,12 @@ async function collectClaudeDocs(roots: AgentSessionRoots, docs: SearchDoc[], bu
|
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
async function collectCodexDocs(roots: AgentSessionRoots, docs: SearchDoc[], budget: ScanBudget) {
|
|
317
|
-
|
|
318
|
-
const
|
|
318
|
+
// Key lanes (P11): every Codex home, the server home first. One root with lanes off.
|
|
319
|
+
const names = await loadCodexThreadNamesAll(roots)
|
|
320
|
+
const pinned = await loadCodexPinnedIdsAll(roots)
|
|
319
321
|
const candidates: Array<{ file: string; mtimeMs: number; birthtimeMs: number }> = []
|
|
320
|
-
|
|
322
|
+
const files = (await Promise.all(codexSessionRootsOf(roots).map(root => listCodexJsonlFiles(root)))).flat()
|
|
323
|
+
for (const file of files) {
|
|
321
324
|
const st = await fileStat(file)
|
|
322
325
|
if (!st?.isFile) continue
|
|
323
326
|
candidates.push({ file, mtimeMs: st.mtimeMs, birthtimeMs: st.birthtimeMs })
|
|
@@ -12,6 +12,7 @@ import { join } from 'node:path'
|
|
|
12
12
|
import { createInterface } from 'node:readline'
|
|
13
13
|
import { promisify } from 'node:util'
|
|
14
14
|
import { codexUserAsk, unwrapPeerMessage } from './session-stream-events.js'
|
|
15
|
+
import { profileCodexHomeSubdirs } from './codex-homes.js'
|
|
15
16
|
|
|
16
17
|
const execFileAsync = promisify(execFile)
|
|
17
18
|
|
|
@@ -66,6 +67,12 @@ export interface AgentSessionRoots {
|
|
|
66
67
|
cursorChats: string
|
|
67
68
|
cursorComposerDb: string
|
|
68
69
|
cursorWorkspaceStorage: string
|
|
70
|
+
/**
|
|
71
|
+
* Key lanes K1 (plan v4 8.2): `data/codex-homes/<id>/sessions` for each keyed Codex profile
|
|
72
|
+
* home. Present only while key lanes are on and a profile home exists; absent otherwise, so
|
|
73
|
+
* the roots (and every list and lookup) are the 963fe25 ones.
|
|
74
|
+
*/
|
|
75
|
+
codexProfileSessions?: string[]
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
export function agentSessionRoots(home = process.env.COS_AGENT_SESSIONS_HOME || homedir()): AgentSessionRoots {
|
|
@@ -78,7 +85,39 @@ export function agentSessionRoots(home = process.env.COS_AGENT_SESSIONS_HOME ||
|
|
|
78
85
|
cursorChats: join(home, '.cursor', 'chats'),
|
|
79
86
|
cursorComposerDb: join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
|
|
80
87
|
cursorWorkspaceStorage: join(home, 'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage'),
|
|
88
|
+
...codexProfileSessionsField(),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function codexProfileSessionsField(): { codexProfileSessions?: string[] } {
|
|
93
|
+
const dirs = profileCodexHomeSubdirs('sessions')
|
|
94
|
+
return dirs.length ? { codexProfileSessions: dirs } : {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The server home's sessions root first, then each profile home's (plan 8.2 codexHomes order). */
|
|
98
|
+
export function codexSessionRootsOf(roots: AgentSessionRoots): string[] {
|
|
99
|
+
return [roots.codexSessions, ...(roots.codexProfileSessions ?? [])]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Key lanes (P11): thread names across every Codex home, the server home's first (it wins a
|
|
104
|
+
* clash). With lanes off there is one root and this is exactly loadCodexThreadNames(server).
|
|
105
|
+
*/
|
|
106
|
+
export async function loadCodexThreadNamesAll(roots: AgentSessionRoots): Promise<Map<string, string>> {
|
|
107
|
+
const [server, ...profiles] = codexSessionRootsOf(roots)
|
|
108
|
+
const names = await loadCodexThreadNames(server!)
|
|
109
|
+
for (const root of profiles) {
|
|
110
|
+
for (const [id, name] of await loadCodexThreadNames(root)) if (!names.has(id)) names.set(id, name)
|
|
81
111
|
}
|
|
112
|
+
return names
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Key lanes (P11): pinned thread ids across every Codex home (one root with lanes off). */
|
|
116
|
+
export async function loadCodexPinnedIdsAll(roots: AgentSessionRoots): Promise<Set<string>> {
|
|
117
|
+
const [server, ...profiles] = codexSessionRootsOf(roots)
|
|
118
|
+
const pinned = await loadCodexPinnedIds(server!)
|
|
119
|
+
for (const root of profiles) for (const id of await loadCodexPinnedIds(root)) pinned.add(id)
|
|
120
|
+
return pinned
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
export function isSafeSessionId(value: string): boolean {
|
|
@@ -1392,7 +1431,7 @@ export async function listAgentSessions(
|
|
|
1392
1431
|
let rows = dedupeSessions([
|
|
1393
1432
|
...enrichedLive,
|
|
1394
1433
|
...await listClaudeSessions(roots.claudeProjects, now, liveIds, cap, starredIds, roots.claudeCodeSessions, dropped, aliases),
|
|
1395
|
-
...await
|
|
1434
|
+
...(await Promise.all(codexSessionRootsOf(roots).map(root => listCodexSessions(root, now, cap, dropped)))).flat(),
|
|
1396
1435
|
...await listCursorSessions(roots.cursorProjects, now, cap, roots.cursorComposerDb, cursorPinned, dropped),
|
|
1397
1436
|
])
|
|
1398
1437
|
if (sort === 'opened') {
|
|
@@ -1458,9 +1497,11 @@ export async function findAgentSessionFile(
|
|
|
1458
1497
|
return matches.length === 1 ? matches[0] : null
|
|
1459
1498
|
}
|
|
1460
1499
|
if (provider === 'codex') {
|
|
1461
|
-
for (const
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1500
|
+
for (const root of codexSessionRootsOf(roots)) {
|
|
1501
|
+
for (const file of await listCodexJsonlFiles(root)) {
|
|
1502
|
+
const name = file.split('/').pop()?.toLowerCase() ?? ''
|
|
1503
|
+
if (name.includes(needle)) return file
|
|
1504
|
+
}
|
|
1464
1505
|
}
|
|
1465
1506
|
return null
|
|
1466
1507
|
}
|
package/server/lib/archive.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { resolveExchangePairModel } from './conversation.js'
|
|
|
19
19
|
import type { ModelPreference } from '../../shared/model-preference.js'
|
|
20
20
|
|
|
21
21
|
import { dataPath } from './data-dir.js'
|
|
22
|
+
import { claudeKeySecretsGuardArgs } from './key-secrets-guard.js'
|
|
22
23
|
const ARCHIVE_DIR = dataPath('archive')
|
|
23
24
|
|
|
24
25
|
// ── Interfaces ──────────────────────────────────────────────
|
|
@@ -193,7 +194,7 @@ function fallbackDaySummary(chats: ArchivedChat[]): string {
|
|
|
193
194
|
*/
|
|
194
195
|
function runClaudeArchiveSummary(input: string, instruction: string): Promise<string> {
|
|
195
196
|
return new Promise((resolve, reject) => {
|
|
196
|
-
const proc = spawn('claude', ['-p', '--model', 'sonnet', instruction], {
|
|
197
|
+
const proc = spawn('claude', ['-p', '--model', 'sonnet', ...claudeKeySecretsGuardArgs(), instruction], {
|
|
197
198
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
198
199
|
})
|
|
199
200
|
|
|
@@ -104,6 +104,8 @@ import { getCodexTrustMode } from './codex-run-ledger.js'
|
|
|
104
104
|
import { CURSOR_SLOT_MODEL_IDS } from './cursor-model-catalog.js'
|
|
105
105
|
import { cursorIsolationUnavailableError, cursorSpawnEnv, isCursorIsolationUnavailable, releaseCursorSpawnOnExit } from './cursor-spawn-env.js'
|
|
106
106
|
import type { PermissionAllowance } from './banned-permission-args.js'
|
|
107
|
+
import { anyKeyedSessions, isKeyedSession } from './keyed-sessions.js'
|
|
108
|
+
import { claudeKeySecretsGuardArgs } from './key-secrets-guard.js'
|
|
107
109
|
import {
|
|
108
110
|
continueAllowanceFor,
|
|
109
111
|
validateContinuePlan,
|
|
@@ -149,6 +151,8 @@ export type AttachedTurnFailure =
|
|
|
149
151
|
| 'adapter_internal_error'
|
|
150
152
|
/** 6.53.0: the caller's abort signal fired (a cancel from the lens). Never ambiguous. */
|
|
151
153
|
| 'cancelled'
|
|
154
|
+
/** Key lanes K1 (plan v4 7.3): the thread ran on a key. Refused before anything is built. */
|
|
155
|
+
| 'keyed_session'
|
|
152
156
|
|
|
153
157
|
/** See the header. The boundary is the first byte handed to the child's stdin. */
|
|
154
158
|
export type AttachedDeliveryState = 'not_attempted' | 'aborted' | 'ambiguous' | 'delivered' | 'cancelled'
|
|
@@ -532,6 +536,7 @@ export function buildClaudeAttachedArgs(nativeThreadId: string): string[] {
|
|
|
532
536
|
// never observe the session id we are required to verify.
|
|
533
537
|
'--verbose',
|
|
534
538
|
'--resume', nativeThreadId,
|
|
539
|
+
...claudeKeySecretsGuardArgs(),
|
|
535
540
|
]
|
|
536
541
|
}
|
|
537
542
|
|
|
@@ -820,6 +825,12 @@ async function run(
|
|
|
820
825
|
}
|
|
821
826
|
const nativeThreadId: string = request.nativeThreadId
|
|
822
827
|
|
|
828
|
+
// Key lanes K1 (plan v4 7.3): a thread that ran on a key is not continued here, where it would
|
|
829
|
+
// resume on the subscription. One failed stat when no keyed session was ever recorded.
|
|
830
|
+
if (anyKeyedSessions() && isKeyedSession(nativeThreadId)) {
|
|
831
|
+
return fail('keyed_session', 'not_attempted', { provider, nativeThreadId, durationMs: duration() })
|
|
832
|
+
}
|
|
833
|
+
|
|
823
834
|
const base = { provider, nativeThreadId, durationMs: 0 }
|
|
824
835
|
|
|
825
836
|
if (!isAttachedPermissionPolicy(request.policy)) {
|