@gotcos/glasses-server 6.48.0 → 6.48.1

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.
@@ -11,6 +11,11 @@ import { installClaudeHooks, uninstallClaudeHooks } from '../lib/claude-hooks-in
11
11
  import { deskIdleSeconds, invalidateHookStatus, sessionHooksHealthFields, sessionSignalStore } from '../lib/session-hooks-runtime.js'
12
12
  import { workspaceFromCwd } from '../lib/claude-session-registry.js'
13
13
 
14
+ /** The registry entrypoints a person sits at; everything else (`sdk-cli`, unknown) may be a job. */
15
+ export function isInteractiveEntrypoint(entrypoint: string | null): boolean {
16
+ return entrypoint === 'claude-desktop' || entrypoint === 'cli'
17
+ }
18
+
14
19
  export function createSessionHooksRouter(options: { port: number }): Router {
15
20
  const router = Router()
16
21
 
@@ -47,11 +52,16 @@ export function createSessionHooksRouter(options: { port: number }): Router {
47
52
  res.set('Cache-Control', 'private, no-store')
48
53
  const since = Number(req.query.since)
49
54
  const sinceMs = Number.isFinite(since) && since > 0 ? since : Date.now() - 24 * 60 * 60_000
55
+ // A Desktop tab or a terminal session is not a run (6.48.1): the ledger wants the
56
+ // `claude -p` jobs (`sdk-cli`). `?all=1` lists every session the hooks saw.
57
+ const all = req.query.all === '1'
50
58
  const runs: Array<Record<string, unknown>> = []
51
59
  for (const signal of sessionSignalStore.snapshot()) {
52
60
  if (signal.firstSeenAt < sinceMs && !(signal.ended && signal.ended.at >= sinceMs)) continue
61
+ if (!all && isInteractiveEntrypoint(signal.entrypoint)) continue
53
62
  runs.push({
54
63
  session_id: signal.sessionId,
64
+ entrypoint: signal.entrypoint,
55
65
  started_at: new Date(signal.firstSeenAt).toISOString(),
56
66
  ended_at: signal.ended ? new Date(signal.ended.at).toISOString() : null,
57
67
  end_reason: signal.ended?.reason ?? null,
@@ -16,6 +16,7 @@
16
16
  // EVERY DEPENDENCY IS INJECTED so the whole path is testable without a live server.
17
17
 
18
18
  import { Router, type Request, type Response } from 'express'
19
+ import { COS_SESSION_ID_RE } from './agent-session-bindings.js'
19
20
  import {
20
21
  admitToQueue, drainDecision, queueableRefusal, queuePosition,
21
22
  MAX_DELIVERY_ATTEMPTS, type DrainObservation, type QueuedThreadTurn,
@@ -27,6 +28,8 @@ export interface ThreadTurnQueueDeps {
27
28
  occupancy: (provider: string, threadId: string) => { attachable: boolean; reason: string | null }
28
29
  /** Did the holder's last transcript record end a turn? */
29
30
  turnEnded: (provider: string, threadId: string) => boolean
31
+ /** 6.48.1, optional: is the holder's turn positively OPEN right now (see DrainObservation.turnOpen)? */
32
+ turnOpen?: (provider: string, threadId: string) => boolean
30
33
  /** The 30s transcript clock, as a backstop. */
31
34
  activity: (provider: string, threadId: string) => 'working' | 'idle' | 'unknown'
32
35
  /**
@@ -77,6 +80,11 @@ function publicRow(turn: QueuedThreadTurn, position: number): Record<string, unk
77
80
  * return false and spend an attempt. A new refusal added upstream is therefore bounded
78
81
  * by default rather than silently retried forever.
79
82
  */
83
+ /** A throwing turnOpen probe is not evidence either way. */
84
+ function safeTurnOpen(deps: ThreadTurnQueueDeps, provider: string, threadId: string): boolean | undefined {
85
+ try { return deps.turnOpen!(provider, threadId) } catch { return undefined }
86
+ }
87
+
80
88
  function isRetryableDelivery(outcome: { reason?: string; serverRetryable?: boolean }): boolean {
81
89
  // The turn route publishes its own verdict; it outranks our inference either way.
82
90
  if (outcome.serverRetryable === false) return false
@@ -116,6 +124,7 @@ export async function drainThread(
116
124
  const seen: DrainObservation = {
117
125
  attachable: gate.attachable,
118
126
  turnEnded: deps.turnEnded(provider, threadId),
127
+ turnOpen: deps.turnOpen ? safeTurnOpen(deps, provider, threadId) : undefined,
119
128
  activity: deps.activity(provider, threadId),
120
129
  reason: gate.reason,
121
130
  }
@@ -212,6 +221,12 @@ export function createThreadTurnQueueRouter(deps: ThreadTurnQueueDeps): Router {
212
221
  if (!clientTurnId || !cosSessionId || !prompt.trim()) {
213
222
  return res.status(400).json({ error: 'invalid_request' })
214
223
  }
224
+ // 6.48.1: the attach route refuses an id outside COS_SESSION_ID_RE as invalid_request,
225
+ // which the drainer treats as retryable, so a bad id used to sit in the queue for the
226
+ // whole TTL. Refuse it here, where the client can still fix it.
227
+ if (!COS_SESSION_ID_RE.test(cosSessionId)) {
228
+ return res.status(400).json({ error: 'invalid_request', reason: 'invalid_cos_session_id' })
229
+ }
215
230
 
216
231
  if (provider === 'cursor') {
217
232
  return res.status(423).json({ error: 'unsupported_provider', queueable: false })