@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
|
@@ -543,11 +543,32 @@ export function codexOwners(
|
|
|
543
543
|
return { owners, doubt }
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Key lanes K1 (plan v4 8.2): the server home's writer locks, then each keyed profile home's.
|
|
548
|
+
* A keyed home that is absent adds nothing (its detector was never the server's to lose); with
|
|
549
|
+
* no profile home this is exactly `codexOwners` on the server home.
|
|
550
|
+
*/
|
|
551
|
+
export function codexOwnersAcrossHomes(threadId: string, probes: OccupancyProbes, dirs: OccupancyDirs): ScanResult {
|
|
552
|
+
const base = codexOwners(threadId, probes, dirs.codexLocksDir)
|
|
553
|
+
if (!dirs.codexProfileLocksDirs) return base
|
|
554
|
+
const owners = [...base.owners]
|
|
555
|
+
let doubt = base.doubt
|
|
556
|
+
for (const dir of dirs.codexProfileLocksDirs) {
|
|
557
|
+
if (!probes.dirExists(dir)) continue
|
|
558
|
+
const keyed = codexOwners(threadId, probes, dir)
|
|
559
|
+
owners.push(...keyed.owners)
|
|
560
|
+
if (keyed.doubt !== null && keyed.doubt !== 'detector_unavailable') doubt ??= keyed.doubt
|
|
561
|
+
}
|
|
562
|
+
return { ...base, owners, doubt }
|
|
563
|
+
}
|
|
564
|
+
|
|
546
565
|
export interface OccupancyDirs {
|
|
547
566
|
/** `<CLAUDE_CONFIG_DIR|~/.claude>/sessions` */
|
|
548
567
|
claudeSessionsDir: string
|
|
549
568
|
/** `<CODEX_HOME|~/.codex>/thread-writer-locks` */
|
|
550
569
|
codexLocksDir: string
|
|
570
|
+
/** Key lanes K1 (plan v4 8.2): each keyed profile home's `thread-writer-locks`. Absent with no profile home. */
|
|
571
|
+
codexProfileLocksDirs?: string[]
|
|
551
572
|
/** `<COS_AGENT_SESSIONS_HOME|~>/.cursor/chats` */
|
|
552
573
|
cursorChatsDir: string
|
|
553
574
|
}
|
|
@@ -698,7 +719,7 @@ export function threadOccupancy(
|
|
|
698
719
|
try {
|
|
699
720
|
result = provider === 'claude'
|
|
700
721
|
? claudeOwners(threadId, probes, dirs.claudeSessionsDir)
|
|
701
|
-
:
|
|
722
|
+
: codexOwnersAcrossHomes(threadId, probes, dirs)
|
|
702
723
|
} catch {
|
|
703
724
|
return { attachable: false, owners: [], reason: 'probe_failed' }
|
|
704
725
|
}
|
|
@@ -7,6 +7,7 @@ import { resolve } from 'node:path'
|
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
8
|
|
|
9
9
|
import { COS_SCRIPTS_DIR } from './python-bridge.js'
|
|
10
|
+
import type { TokenAuditKeyFields } from './key-relay-contract.js'
|
|
10
11
|
|
|
11
12
|
// Shared JSONL file — same path as Python's llm_client.py uses
|
|
12
13
|
// Uses COS_SCRIPTS_DIR when available (server mode), falls back to user home
|
|
@@ -21,7 +22,7 @@ function estimateTokens(chars: number): number {
|
|
|
21
22
|
return Math.max(1, Math.floor(chars / CHARS_PER_TOKEN))
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export interface TokenAuditEntry {
|
|
25
|
+
export interface TokenAuditEntry extends TokenAuditKeyFields {
|
|
25
26
|
source: string // "g2-voice", "g2-prewarm", "g2-archive", "g2-query"
|
|
26
27
|
model: string // "opus", "sonnet", "haiku", "cursor-composer", ...
|
|
27
28
|
inputChars: number
|
|
@@ -35,9 +36,14 @@ export interface TokenAuditEntry {
|
|
|
35
36
|
usageKind?: 'provider_final' | 'estimated'
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The JSONL record. Key-lane fields (plan v4 3.1: `profile`, `lane`, `credentialKind`, `usd`) are
|
|
41
|
+
* conditional spreads AFTER every 6.79.0 key, written only when the caller sets them, so a Lane A
|
|
42
|
+
* (subscription) row is byte-identical to 6.79.0. Callers set them only on keyed turns.
|
|
43
|
+
*/
|
|
44
|
+
export function buildTokenAuditRecord(entry: TokenAuditEntry, now: Date = new Date()): Record<string, unknown> {
|
|
45
|
+
return {
|
|
46
|
+
ts: now.toISOString(),
|
|
41
47
|
source: entry.source,
|
|
42
48
|
model: entry.model,
|
|
43
49
|
input_chars: entry.inputChars,
|
|
@@ -51,7 +57,15 @@ export function logTokenAudit(entry: TokenAuditEntry): void {
|
|
|
51
57
|
...(entry.runId ? { run_id: entry.runId } : {}),
|
|
52
58
|
...(entry.sessionId ? { session_id: entry.sessionId } : {}),
|
|
53
59
|
...(entry.clientJobId ? { client_job_id: entry.clientJobId } : {}),
|
|
60
|
+
...(entry.profile ? { profile: entry.profile } : {}),
|
|
61
|
+
...(entry.lane ? { lane: entry.lane } : {}),
|
|
62
|
+
...(entry.credentialKind ? { credentialKind: entry.credentialKind } : {}),
|
|
63
|
+
...(typeof entry.usd === 'number' && Number.isFinite(entry.usd) ? { usd: entry.usd } : {}),
|
|
54
64
|
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function logTokenAudit(entry: TokenAuditEntry): void {
|
|
68
|
+
const record = buildTokenAuditRecord(entry)
|
|
55
69
|
try {
|
|
56
70
|
appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
57
71
|
chmodSync(AUDIT_FILE, 0o600)
|
|
@@ -23,6 +23,7 @@ import { isSafeDomainName } from './domains.js'
|
|
|
23
23
|
import { acquireServerInstanceLock, type ServerInstanceLock } from './server-instance-lock.js'
|
|
24
24
|
import { parseWorkSessionId, modelProvider, UUID_V4, FORK_PROVIDERS } from './work-activity.js'
|
|
25
25
|
import { normalizeModelPreference } from '../../shared/model-preference.js'
|
|
26
|
+
import { KEY_PROFILE_REJECT_CODE, KEY_PROFILE_WORK_REJECT, keyProfileRejectApplies } from './key-profile-rejects.js'
|
|
26
27
|
|
|
27
28
|
export class WorkHandoffRequestError extends Error {
|
|
28
29
|
/** `extra` goes into the error body next to the code (the daily cap's `resetsAt`). */
|
|
@@ -136,6 +137,8 @@ export function parseHandoffRequest(body: unknown, defaultModel: string): { inpu
|
|
|
136
137
|
let model: string | undefined
|
|
137
138
|
if (given(body.model)) {
|
|
138
139
|
model = normalizeModelPreference(body.model)
|
|
140
|
+
// Key lanes K1 (plan 5.2): Reject, and say why. Work hands off subscription sessions only.
|
|
141
|
+
if (!model && keyProfileRejectApplies(body.model)) throw new WorkHandoffRequestError(KEY_PROFILE_REJECT_CODE, 400, KEY_PROFILE_WORK_REJECT)
|
|
139
142
|
if (!model) throw invalid('Choose a model slot this server knows.')
|
|
140
143
|
}
|
|
141
144
|
let note: string | undefined
|
|
@@ -131,6 +131,7 @@ import { isValidNativeThreadId } from '../lib/native-thread-id.js'
|
|
|
131
131
|
import { FORK_PROVIDER_FAILURES, type ForkProviderFailure } from '../lib/fork-thread.js'
|
|
132
132
|
import { CLIENT_FORK_ID_RE, ForkJobLedger, type ForkJobRow } from '../lib/fork-job-ledger.js'
|
|
133
133
|
import { PEER_VERIFY_TIMEOUT_MS } from '../lib/session-peer-inbox.js'
|
|
134
|
+
import { KEYED_SESSION_REFUSAL } from '../lib/keyed-sessions.js'
|
|
134
135
|
import {
|
|
135
136
|
CANCEL_QUEUE_HOLD_MS,
|
|
136
137
|
CLIENT_CANCEL_ID_RE,
|
|
@@ -652,6 +653,9 @@ export type WriteRefusal =
|
|
|
652
653
|
// person said stop. Deliberately NOT a fence (plan validation B2): the next Continue
|
|
653
654
|
// must work, and it re-reads the head.
|
|
654
655
|
| 'turn_cancelled'
|
|
656
|
+
// Key lanes (P3, wired by P11): the thread ran on a key profile. Refused before any spawn, with
|
|
657
|
+
// the same 409 body every other keyed entry point answers (keyed-sessions.ts).
|
|
658
|
+
| 'keyed_session'
|
|
655
659
|
// ------------------------------------------------------------------- fork
|
|
656
660
|
//
|
|
657
661
|
// Fork gets its OWN members rather than reusing the ones above, and the reason
|
|
@@ -739,6 +743,7 @@ export const WRITE_REASON_COPY: Record<Exclude<WriteRefusal, OccupancyReason>, s
|
|
|
739
743
|
'COS could not run this turn. Nothing was sent. You can try again.',
|
|
740
744
|
turn_cancelled:
|
|
741
745
|
'Run cancelled.',
|
|
746
|
+
keyed_session: KEYED_SESSION_REFUSAL.message,
|
|
742
747
|
|
|
743
748
|
// Fork copy. No sentence here may end with "Fork it instead" — this IS the fork,
|
|
744
749
|
// and pointing a failed fork back at itself is a dead end rather than an action.
|
|
@@ -2698,6 +2703,10 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
|
|
|
2698
2703
|
return refuseFork('fork_orphan_possible', { orphanPossible: true })
|
|
2699
2704
|
}
|
|
2700
2705
|
|
|
2706
|
+
// Key lanes: fork-thread refused a keyed source before any spawn (P3). The exact keyed 409.
|
|
2707
|
+
if (raw && typeof raw === 'object' && (raw as { ok?: unknown }).ok === false && (raw as { reason?: unknown }).reason === 'keyed_session') {
|
|
2708
|
+
return refuseFork('keyed_session', { ...KEYED_SESSION_REFUSAL, retryable: false })
|
|
2709
|
+
}
|
|
2701
2710
|
const outcome = classifyFork(raw, threadIdParam)
|
|
2702
2711
|
{
|
|
2703
2712
|
// 6.62.1: one line per attempt. On 2026-10-05 a fork's real cause existed only
|
|
@@ -3458,6 +3467,12 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
|
|
|
3458
3467
|
return refuseTurn('turn_cancelled', { retryable: false, deliveryState: 'cancelled' })
|
|
3459
3468
|
}
|
|
3460
3469
|
|
|
3470
|
+
// Key lanes: the adapter refused a keyed thread before any spawn (P3). The exact keyed 409.
|
|
3471
|
+
if (adapterEvidence.adapterReason === 'keyed_session') {
|
|
3472
|
+
spawnMay = false // refused before any spawn
|
|
3473
|
+
return refuseTurn('keyed_session', { ...KEYED_SESSION_REFUSAL, retryable: false })
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3461
3476
|
if (delivery.kind === 'aborted') {
|
|
3462
3477
|
// The adapter's own proof that the child never opened the session.
|
|
3463
3478
|
spawnMay = false
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { workObservationFields } from '../lib/session-work-observation.js'
|
|
2
|
-
import {
|
|
2
|
+
import { readCodexSubagentsAcross } from '../lib/codex-subagent-activity.js'
|
|
3
|
+
import { listKeyedSessions } from '../lib/keyed-sessions.js'
|
|
4
|
+
import { codexProfileHome } from '../lib/codex-homes.js'
|
|
5
|
+
import { join as joinPath } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/** Key lanes (P11): a keyed Codex thread's own sessions root (P3's keyed-session record). */
|
|
8
|
+
function keyedCodexRootOf(): (id: string) => string | undefined {
|
|
9
|
+
const byId = new Map(listKeyedSessions().filter(r => r.engine === 'codex').map(r => [r.id.toLowerCase(), joinPath(codexProfileHome(r.profileId), 'sessions')]))
|
|
10
|
+
return id => byId.get(id.toLowerCase())
|
|
11
|
+
}
|
|
3
12
|
// GET /api/agent-sessions
|
|
4
13
|
// GET /api/agent-sessions/search?q=
|
|
5
14
|
// GET /api/agent-sessions/:provider/:sessionId
|
|
@@ -31,7 +40,8 @@ import {
|
|
|
31
40
|
emptySessionListDropped,
|
|
32
41
|
loadClaudeDesktopAliases,
|
|
33
42
|
loadCursorComposerNames,
|
|
34
|
-
|
|
43
|
+
codexSessionRootsOf,
|
|
44
|
+
loadCodexThreadNamesAll,
|
|
35
45
|
idFromCodexFilename,
|
|
36
46
|
parseAgentSession,
|
|
37
47
|
type AgentProvider,
|
|
@@ -518,7 +528,8 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
|
|
|
518
528
|
derivedById.set(row.session_id, deriveCodexRow(row.session_id, running.occupied.has(row.session_id), facts, now))
|
|
519
529
|
}
|
|
520
530
|
const queuedOf = await allQueuedWaitingLookup(now)
|
|
521
|
-
const
|
|
531
|
+
const codexRoots = codexSessionRootsOf(roots)
|
|
532
|
+
const subagents = await readCodexSubagentsAcross(sessions.filter(row => row.provider === 'codex').map(row => row.session_id), codexRoots, now, codexRoots.length > 1 ? keyedCodexRootOf() : undefined)
|
|
522
533
|
res.json({
|
|
523
534
|
sessions: sessions.map((row, index) => {
|
|
524
535
|
const derived = derivedById.get(row.session_id)
|
|
@@ -624,7 +635,7 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
|
|
|
624
635
|
// parent's id (see `listCodexSessions`), and the parent's name would be the wrong one.
|
|
625
636
|
let codexThreadName: string | undefined
|
|
626
637
|
if (provider === 'codex') {
|
|
627
|
-
const names = await
|
|
638
|
+
const names = await loadCodexThreadNamesAll(agentSessionRoots())
|
|
628
639
|
const fileId = idFromCodexFilename(found.split('/').pop() || '')
|
|
629
640
|
codexThreadName = (fileId ? names.get(fileId) : undefined) || names.get(sessionId) || undefined
|
|
630
641
|
}
|
|
@@ -649,7 +660,8 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
|
|
|
649
660
|
const now = Date.now()
|
|
650
661
|
const rollout = provider === 'codex' ? codexRolloutFacts(found, now) : null
|
|
651
662
|
const observed = provider === 'codex' ? codexCompactionFields(workObservationFor(parsed.session_id), rollout, now) : workObservationFields(workObservationFor(parsed.session_id))
|
|
652
|
-
const
|
|
663
|
+
const detailRoots = provider === 'codex' ? codexSessionRootsOf(agentSessionRoots()) : []
|
|
664
|
+
const subagents = provider === 'codex' ? (await readCodexSubagentsAcross([sessionId], detailRoots, undefined, detailRoots.length > 1 ? keyedCodexRootOf() : undefined)).get(sessionId.toLowerCase()) : observed.subagent_activity
|
|
653
665
|
const modified = st.mtime.toISOString()
|
|
654
666
|
const activity = await readSessionActivity(provider, found)
|
|
655
667
|
const running = runningForThread(provider, parsed.session_id, activityClockMs(st.mtimeMs, activity))
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { getAvailableCliSessionId } from '../lib/claude-bridge.js'
|
|
11
11
|
import { getCodexExecutionCwd, getCodexTrustMode } from '../lib/codex-run-ledger.js'
|
|
12
12
|
import { normalizeHandoffCode } from '../../shared/handoff-intent.js'
|
|
13
|
+
import { anyKeyedSessions, isKeyedCosSession, isKeyedSession, KEYED_SESSION_REFUSAL } from '../lib/keyed-sessions.js'
|
|
13
14
|
|
|
14
15
|
export const handoffsRouter = Router()
|
|
15
16
|
|
|
@@ -17,6 +18,20 @@ function runtimeExpiresAt(): string {
|
|
|
17
18
|
return new Date(Date.now() + 2 * 60 * 60_000).toISOString()
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Key lanes K1 (plan v4 7.1, 7.3): a handoff carries a CLI session id that another surface
|
|
23
|
+
* resumes, on the subscription. One that ran on a key is refused in K1. With no keyed session
|
|
24
|
+
* ever recorded this is one failed stat and the route is the b7c620e one.
|
|
25
|
+
*/
|
|
26
|
+
function keyedHandoff(body: Record<string, any> | null | undefined, resolvedCliSessionId?: string): boolean {
|
|
27
|
+
if (!anyKeyedSessions()) return false
|
|
28
|
+
const runtime = body?.runtime && typeof body.runtime === 'object' ? body.runtime : {}
|
|
29
|
+
return isKeyedCosSession(body?.sessionId)
|
|
30
|
+
|| isKeyedSession(runtime.claude?.cliSessionId)
|
|
31
|
+
|| isKeyedSession(runtime.codex?.codexThreadId)
|
|
32
|
+
|| isKeyedSession(resolvedCliSessionId)
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function enrichRuntime(body: Record<string, any>): HandoffCreateInput {
|
|
21
36
|
const input: HandoffCreateInput = { ...body }
|
|
22
37
|
const runtime = body.runtime && typeof body.runtime === 'object' ? { ...body.runtime } : {}
|
|
@@ -53,7 +68,9 @@ function enrichRuntime(body: Record<string, any>): HandoffCreateInput {
|
|
|
53
68
|
handoffsRouter.post('/handoffs', async (req, res) => {
|
|
54
69
|
try {
|
|
55
70
|
const body = req.body && typeof req.body === 'object' ? req.body : {}
|
|
56
|
-
const
|
|
71
|
+
const input = enrichRuntime(body)
|
|
72
|
+
if (keyedHandoff(body, (input.runtime as any)?.claude?.cliSessionId)) return res.status(409).json(KEYED_SESSION_REFUSAL)
|
|
73
|
+
const handoff = await createHandoff(input)
|
|
57
74
|
res.status(201).json({ handoff, context: buildHandoffPromptContext(handoff) })
|
|
58
75
|
} catch (err: any) {
|
|
59
76
|
res.status(500).json({ error: err?.message ?? 'handoff create failed' })
|
|
@@ -92,6 +109,10 @@ handoffsRouter.post('/handoffs/:code/claim', async (req, res) => {
|
|
|
92
109
|
if (!code) return res.status(404).json({ error: 'handoff not found' })
|
|
93
110
|
try {
|
|
94
111
|
const claimedBy = typeof req.body?.claimedBy === 'string' ? req.body.claimedBy : 'unknown'
|
|
112
|
+
// Key lanes: a handoff saved before its session was known to be keyed is not claimed either.
|
|
113
|
+
if (anyKeyedSessions() && keyedHandoff((await getHandoff(code)) as Record<string, any> | null)) {
|
|
114
|
+
return res.status(409).json(KEYED_SESSION_REFUSAL)
|
|
115
|
+
}
|
|
95
116
|
const handoff = await claimHandoff(code, claimedBy)
|
|
96
117
|
if (!handoff) return res.status(404).json({ error: 'handoff not found' })
|
|
97
118
|
res.json({ handoff, context: buildHandoffPromptContext(handoff) })
|
package/server/routes/health.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { meetingAskCapabilities } from '../lib/meeting-ask-runtime.js'
|
|
|
32
32
|
import { lensGistPublicHealth } from '../lib/lens-gist.js'
|
|
33
33
|
import { meetingSummaryLLMEnabled } from '../lib/meeting-summary.js'
|
|
34
34
|
import { getKeyStatus } from '../lib/openai-key.js'
|
|
35
|
+
import { keyLanesHealthFields, keyProfilesModelsField } from '../lib/key-lanes.js'
|
|
35
36
|
import {
|
|
36
37
|
getCodexModelCatalog,
|
|
37
38
|
getCodexModelCatalogSnapshot,
|
|
@@ -550,6 +551,8 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
550
551
|
// count only (health is public); the items and their Retry are behind the token at
|
|
551
552
|
// GET /api/maintenance/status.
|
|
552
553
|
crash_loop_parking: crashLoopParkingHealth(),
|
|
554
|
+
// Key lanes (plan v4 3.1): `details.keyLanes` only while enabled. Absent, never null, when off.
|
|
555
|
+
...keyLanesHealthFields(),
|
|
553
556
|
})
|
|
554
557
|
})
|
|
555
558
|
|
|
@@ -616,6 +619,8 @@ healthRouter.get('/models', async (req, res) => {
|
|
|
616
619
|
refreshedAt: ollamaCatalog.refreshedAt,
|
|
617
620
|
},
|
|
618
621
|
ollamaReady: isOllamaProviderReady(),
|
|
622
|
+
// Key lanes (plan v4 3.1): `keyProfiles` only while a profile is active. Absent when off.
|
|
623
|
+
...keyProfilesModelsField(),
|
|
619
624
|
serverInstanceId: getServerInstanceId(),
|
|
620
625
|
// Minted here rather than on a route of its own: this response is already
|
|
621
626
|
// authenticated, and the mint is stateless, so it costs nothing and stores
|