@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.
Files changed (34) hide show
  1. package/.env.example +16 -0
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +56 -3
  4. package/managed-runtime-contract.json +1 -0
  5. package/package.json +1 -1
  6. package/server/index.ts +58 -9
  7. package/server/lib/activity-preview.ts +45 -2
  8. package/server/lib/agent-session-search.ts +3 -1
  9. package/server/lib/agent-session-store.ts +35 -5
  10. package/server/lib/agent-session-turns.ts +4 -1
  11. package/server/lib/claude-bridge.ts +9 -0
  12. package/server/lib/client-instance-claim.ts +22 -1
  13. package/server/lib/client-liveness.ts +50 -0
  14. package/server/lib/codex-bridge.ts +6 -0
  15. package/server/lib/cursor-bridge.ts +6 -0
  16. package/server/lib/job-trail.ts +758 -0
  17. package/server/lib/ollama-bridge.ts +17 -0
  18. package/server/lib/permission-broker.ts +1156 -0
  19. package/server/lib/query-job-coordinator.ts +17 -1
  20. package/server/lib/query-job-feature.ts +10 -0
  21. package/server/lib/query-job-runtime.ts +9 -1
  22. package/server/lib/query-job-store.ts +214 -5
  23. package/server/lib/query-job-types.ts +36 -0
  24. package/server/lib/session-signal-store.ts +42 -10
  25. package/server/lib/session-state-derive.ts +14 -4
  26. package/server/lib/session-stream-events.ts +629 -18
  27. package/server/lib/session-stream-producer.ts +25 -6
  28. package/server/lib/session-transcript-watcher.ts +120 -3
  29. package/server/routes/agent-sessions.ts +12 -1
  30. package/server/routes/client-instance.ts +12 -1
  31. package/server/routes/cursor-stop-followup.ts +14 -7
  32. package/server/routes/health.ts +24 -1
  33. package/server/routes/permission-broker.ts +176 -0
  34. 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
- if (now - owner.seenAt > CLIENT_INSTANCE_LIVE_MS) return { owner: fresh, verdict: 'owner', took: true }
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()