@gotcos/glasses-server 6.51.0 → 6.52.3
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 +16 -0
- package/CHANGELOG.md +86 -0
- package/README.md +56 -3
- package/managed-runtime-contract.json +1 -0
- package/package.json +1 -1
- package/server/index.ts +58 -9
- package/server/lib/activity-preview.ts +45 -2
- package/server/lib/agent-session-search.ts +3 -1
- package/server/lib/agent-session-store.ts +35 -5
- package/server/lib/agent-session-turns.ts +4 -1
- package/server/lib/claude-bridge.ts +9 -0
- package/server/lib/client-instance-claim.ts +22 -1
- package/server/lib/client-liveness.ts +50 -0
- package/server/lib/codex-bridge.ts +6 -0
- package/server/lib/cursor-bridge.ts +6 -0
- package/server/lib/job-trail.ts +758 -0
- package/server/lib/ollama-bridge.ts +17 -0
- package/server/lib/permission-broker.ts +1156 -0
- package/server/lib/query-job-coordinator.ts +17 -1
- package/server/lib/query-job-feature.ts +10 -0
- package/server/lib/query-job-runtime.ts +9 -1
- package/server/lib/query-job-store.ts +214 -5
- package/server/lib/query-job-types.ts +36 -0
- package/server/lib/session-signal-store.ts +42 -10
- package/server/lib/session-state-derive.ts +14 -4
- package/server/lib/session-stream-events.ts +629 -18
- package/server/lib/session-stream-producer.ts +25 -6
- package/server/lib/session-transcript-watcher.ts +120 -3
- package/server/routes/agent-sessions.ts +12 -1
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +14 -7
- package/server/routes/health.ts +24 -1
- package/server/routes/permission-broker.ts +176 -0
- package/server/routes/query-jobs.ts +84 -13
|
@@ -16,6 +16,20 @@
|
|
|
16
16
|
export const CLIENT_INSTANCE_TICK_MS = 15_000
|
|
17
17
|
/** Three missed ticks: the owner is gone. */
|
|
18
18
|
export const CLIENT_INSTANCE_LIVE_MS = 3 * CLIENT_INSTANCE_TICK_MS
|
|
19
|
+
/**
|
|
20
|
+
* 6.52.3: how long the owner must be quiet before an OLDER boot may take the ring back.
|
|
21
|
+
* On 2026-09-20 a hidden copy from the night before took the ring after the live copy
|
|
22
|
+
* went quiet (a locked phone throttles its timers) and answered a hold-to-talk 27 minutes
|
|
23
|
+
* later with a stale session. 45 s is a normal quiet spell for a locked phone; a dead
|
|
24
|
+
* copy stays dead, so waiting ten minutes costs nothing.
|
|
25
|
+
*/
|
|
26
|
+
export const CLIENT_INSTANCE_RECLAIM_MS = 10 * 60_000
|
|
27
|
+
/**
|
|
28
|
+
* 6.52.3: an older boot may take the ring back only when ITS OWN previous claim is this
|
|
29
|
+
* recent. Both copies sleep when the phone does; a zombie that woke up alongside the live
|
|
30
|
+
* copy has a gap of its own, and the live copy's refresh lands within a tick.
|
|
31
|
+
*/
|
|
32
|
+
export const CLIENT_INSTANCE_AWAKE_MS = CLIENT_INSTANCE_LIVE_MS
|
|
19
33
|
/**
|
|
20
34
|
* 6.50.4: a meeting chunk inside this window means that copy is recording. Five chunk
|
|
21
35
|
* intervals (~6 s each), so a stretch of silence or a slow upload does not end the hold.
|
|
@@ -126,6 +140,8 @@ export function arbitrateClientInstance(
|
|
|
126
140
|
claim: ClientInstanceClaim,
|
|
127
141
|
now: number,
|
|
128
142
|
evidence: CaptureEvidence = NO_CAPTURE,
|
|
143
|
+
/** When this claimant last claimed (any verdict), or undefined: never seen. */
|
|
144
|
+
claimantLastSeenAt?: number,
|
|
129
145
|
): { owner: ClientInstanceOwner; verdict: ClientInstanceVerdict; took: boolean } {
|
|
130
146
|
const fresh: ClientInstanceOwner = { ...claim, seenAt: now }
|
|
131
147
|
if (!owner) return { owner: fresh, verdict: 'owner', took: true }
|
|
@@ -142,6 +158,11 @@ export function arbitrateClientInstance(
|
|
|
142
158
|
// The ring moves on the newer copy's first claim after the meeting stops.
|
|
143
159
|
if (ownerIsRecording(owner, evidence, now)) return { owner, verdict: 'yield', took: false }
|
|
144
160
|
if (isNewer(claim, owner)) return { owner: fresh, verdict: 'owner', took: true }
|
|
145
|
-
|
|
161
|
+
// 6.52.3: an older boot takes a quiet ring back only after CLIENT_INSTANCE_RECLAIM_MS,
|
|
162
|
+
// and only while it has itself been claiming (awake), never on the first tick after a
|
|
163
|
+
// gap of its own. A dead newer copy is replaced when the user reopens COS (a newer boot
|
|
164
|
+
// still takes at once); this rule is for the copy that is merely quiet.
|
|
165
|
+
const claimantAwake = claimantLastSeenAt !== undefined && now - claimantLastSeenAt <= CLIENT_INSTANCE_AWAKE_MS
|
|
166
|
+
if (now - owner.seenAt > CLIENT_INSTANCE_RECLAIM_MS && claimantAwake) return { owner: fresh, verdict: 'owner', took: true }
|
|
146
167
|
return { owner, verdict: 'yield', took: false }
|
|
147
168
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// When a client that can ANSWER a session question last asked for them (6.52.0).
|
|
2
|
+
//
|
|
3
|
+
// THE SIGNAL, AND WHY THIS ONE. The permission broker holds a session question or a tool
|
|
4
|
+
// approval only while a lens or phone client could show it and send the answer back. The
|
|
5
|
+
// only proof of that is a client asking for the questions: an authenticated
|
|
6
|
+
// `GET /api/session-questions?client=glasses|phone` (X-Cos-Token, never the hook token)
|
|
7
|
+
// within the last 30 s, checked at admission and again while anything is held.
|
|
8
|
+
//
|
|
9
|
+
// Rejected, and why:
|
|
10
|
+
// - the client-instance claim (`POST /api/client-instance/claim`): every COS Glasses copy
|
|
11
|
+
// since 6.9.505 posts it every 15 s, including the ones with no question UI. Counting
|
|
12
|
+
// it made a 6.9.511 app on the phone read as "someone can answer", so the Mac held
|
|
13
|
+
// questions and approvals nobody could see for up to 110 s (coordinator review,
|
|
14
|
+
// 2026-09-19). A claim says a copy is alive, not that it can answer.
|
|
15
|
+
// - the display stream: an open socket is not alive (a half-open connection, a
|
|
16
|
+
// `probe=1` connect, COS Control and curl all read the same).
|
|
17
|
+
// - client diagnostics and meeting chunks: lossy, or only while recording.
|
|
18
|
+
//
|
|
19
|
+
// So an app that has never heard of this feature never calls the route, and the broker
|
|
20
|
+
// answers `{}` (the Mac's own dialog) for it, exactly as before. The poll also bounds
|
|
21
|
+
// delivery: a copy whose timers are frozen (a locked phone) cannot poll, and could not
|
|
22
|
+
// show the question either.
|
|
23
|
+
//
|
|
24
|
+
// In memory: a restart forgets it, and the next poll restores it. Until then nothing is
|
|
25
|
+
// held, which is the native dialog, the safe side.
|
|
26
|
+
|
|
27
|
+
let lastPollAt: number | null = null
|
|
28
|
+
|
|
29
|
+
/** A stored stamp this far ahead of a new poll means the clock stepped back. */
|
|
30
|
+
const CLOCK_STEP_BACK_MS = 5_000
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A counting questions poll answered at `at` (server clock). Never moves backwards for a
|
|
34
|
+
* slower request answered later, but a clock that stepped back is followed: a stamp left in
|
|
35
|
+
* the future would read stale to the broker, and ignoring every poll until the clock caught
|
|
36
|
+
* up would leave no live client for that long.
|
|
37
|
+
*/
|
|
38
|
+
export function noteQuestionsPoll(at: number): void {
|
|
39
|
+
if (!Number.isFinite(at)) return
|
|
40
|
+
if (lastPollAt === null || at > lastPollAt || lastPollAt - at > CLOCK_STEP_BACK_MS) lastPollAt = at
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** When the newest authenticated questions poll arrived, or null when none has since boot. */
|
|
44
|
+
export function lastQuestionsPollAt(): number | null {
|
|
45
|
+
return lastPollAt
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function __resetClientLivenessForTests(): void {
|
|
49
|
+
lastPollAt = null
|
|
50
|
+
}
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
} from '../../shared/media-attachment.js'
|
|
85
85
|
import { terminalProviderAuthFailure } from './provider-terminal-error.js'
|
|
86
86
|
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
87
|
+
import { teeJobTrail } from './job-trail.js'
|
|
87
88
|
|
|
88
89
|
const INACTIVITY_MS = 180_000
|
|
89
90
|
const WALL_MAX_MS = 900_000
|
|
@@ -833,6 +834,11 @@ export async function callCodexStreaming(
|
|
|
833
834
|
}
|
|
834
835
|
})
|
|
835
836
|
|
|
837
|
+
// 6.52.0: the Messages trail, a SECOND stdout reader registered after the one above, so
|
|
838
|
+
// that handler sees every chunk first and nothing it decides can change. Without an
|
|
839
|
+
// `onTrail` (legacy /api/query, openai-compat, COS_MESSAGES_TRAIL=0) this attaches nothing.
|
|
840
|
+
teeJobTrail(proc.stdout, 'codex', callbacks.onTrail)
|
|
841
|
+
|
|
836
842
|
proc.stderr.on('data', (chunk: Buffer) => {
|
|
837
843
|
resetInactivity()
|
|
838
844
|
stderr += chunk.toString()
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
type MediaAttachmentRef,
|
|
64
64
|
} from '../../shared/media-attachment.js'
|
|
65
65
|
import { terminalProviderAuthFailure } from './provider-terminal-error.js'
|
|
66
|
+
import { teeJobTrail } from './job-trail.js'
|
|
66
67
|
|
|
67
68
|
const INACTIVITY_MS = 180_000
|
|
68
69
|
const WALL_MAX_MS = 900_000
|
|
@@ -666,6 +667,11 @@ export async function callCursorStreaming(
|
|
|
666
667
|
}
|
|
667
668
|
})
|
|
668
669
|
|
|
670
|
+
// 6.52.0: the Messages trail, a SECOND stdout reader registered after the one above, so
|
|
671
|
+
// that handler sees every chunk first and nothing it decides can change. Without an
|
|
672
|
+
// `onTrail` (legacy /api/query, openai-compat, COS_MESSAGES_TRAIL=0) this attaches nothing.
|
|
673
|
+
teeJobTrail(proc.stdout, 'cursor', callbacks.onTrail)
|
|
674
|
+
|
|
669
675
|
proc.stderr.on('data', (chunk: Buffer) => {
|
|
670
676
|
resetInactivity()
|
|
671
677
|
stderr += chunk.toString()
|