@gotcos/glasses-server 6.50.4 → 6.52.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.
Files changed (41) hide show
  1. package/.env.example +16 -0
  2. package/CHANGELOG.md +78 -0
  3. package/README.md +56 -3
  4. package/bin/hooks/cos-session-hook +24 -1
  5. package/managed-runtime-contract.json +1 -0
  6. package/package.json +1 -1
  7. package/server/index.ts +83 -5
  8. package/server/lib/activity-preview.ts +45 -2
  9. package/server/lib/agent-session-search.ts +3 -1
  10. package/server/lib/agent-session-store.ts +35 -5
  11. package/server/lib/agent-session-turns.ts +4 -1
  12. package/server/lib/claude-bridge.ts +9 -0
  13. package/server/lib/client-liveness.ts +50 -0
  14. package/server/lib/codex-bridge.ts +6 -0
  15. package/server/lib/codex-live-queue-deps.ts +135 -0
  16. package/server/lib/codex-live-queue.ts +232 -0
  17. package/server/lib/codex-turn-clock.ts +36 -0
  18. package/server/lib/cursor-bridge.ts +6 -0
  19. package/server/lib/cursor-stop-followup.ts +134 -0
  20. package/server/lib/job-trail.ts +758 -0
  21. package/server/lib/occupancy-probes.ts +52 -0
  22. package/server/lib/ollama-bridge.ts +17 -0
  23. package/server/lib/permission-broker.ts +1156 -0
  24. package/server/lib/query-job-coordinator.ts +17 -1
  25. package/server/lib/query-job-feature.ts +10 -0
  26. package/server/lib/query-job-runtime.ts +9 -1
  27. package/server/lib/query-job-store.ts +214 -5
  28. package/server/lib/query-job-types.ts +36 -0
  29. package/server/lib/session-signal-store.ts +42 -10
  30. package/server/lib/session-state-derive.ts +14 -4
  31. package/server/lib/session-stream-events.ts +600 -15
  32. package/server/lib/session-stream-producer.ts +25 -6
  33. package/server/lib/session-transcript-watcher.ts +120 -3
  34. package/server/lib/thread-occupancy.ts +30 -7
  35. package/server/routes/agent-session-bindings.ts +98 -6
  36. package/server/routes/agent-sessions.ts +24 -2
  37. package/server/routes/cursor-stop-followup.ts +74 -0
  38. package/server/routes/health.ts +34 -1
  39. package/server/routes/permission-broker.ts +176 -0
  40. package/server/routes/query-jobs.ts +84 -13
  41. package/server/routes/thread-turn-queue.ts +15 -5
@@ -0,0 +1,134 @@
1
+ // A queued turn for a Cursor chat, handed to Cursor by its own Stop hook (6.51.0).
2
+ //
3
+ // ---------------------------------------------------------------------------
4
+ // WHY THE STOP HOOK, AND ONLY THE STOP HOOK
5
+ // ---------------------------------------------------------------------------
6
+ // Cursor has no `queue --thread` and no socket a process outside it can write into. A
7
+ // Cursor IDE composer cannot be resumed from outside either: Gate 0 (2026-08-20) found
8
+ // `--resume` on an IDE transcript to be a clobber / wrong-store landmine, and that stays
9
+ // refused. But Cursor 3.21 runs a `stop` hook when a composer's turn ends, and when the
10
+ // hook answers `{"followup_message": "..."}` Cursor submits that text into THE SAME
11
+ // composer as the next message (`submitChatMaybeAbortCurrent(composerId, text,
12
+ // {isAutoFollowupFromStopHook: true})`, read from the shipped workbench). That is the
13
+ // exact shape of "deliver when the current turn ends", from inside the composer.
14
+ //
15
+ // And the hook is already there: Cursor reads `~/.claude/settings.json` hooks by default
16
+ // (its third-party extensibility setting), so the COS session hook installed on 9/15 has
17
+ // been running for Cursor composers since then -- 478 Cursor events in the ledger by
18
+ // 9/18, stdin = Cursor's own payload (`conversation_id`, `status`, `loop_count`,
19
+ // `cursor_version`), environment `CURSOR_VERSION`. Nothing new is installed for Cursor;
20
+ // the one script gains one Cursor-only branch.
21
+ //
22
+ // ---------------------------------------------------------------------------
23
+ // WHAT THIS CANNOT DO, SAID PLAINLY
24
+ // ---------------------------------------------------------------------------
25
+ // It delivers only at the END OF A TURN. An idle composer fires no Stop, so a turn cannot
26
+ // be put into it from outside; the queue admits a Cursor turn only while the hooks show
27
+ // that composer mid-turn (`native_thread_working`), and a turn that is never claimed
28
+ // expires on the queue's ordinary clock and says so. Cursor also caps consecutive hook
29
+ // follow-ups (its default loop limit, reset by any message typed in Cursor), so a long
30
+ // queue can stop short; the rest waits for the next typed message and its Stop.
31
+ //
32
+ // ---------------------------------------------------------------------------
33
+ // ONE DELIVERER
34
+ // ---------------------------------------------------------------------------
35
+ // The queue drainer never delivers a Cursor turn (see `drainThread`): a turn could
36
+ // otherwise be claimed here and delivered there in the same second. Claiming marks the
37
+ // row `delivered` before the text leaves, and a Stop that is not `completed` (aborted,
38
+ // error) claims nothing -- a queued sentence must not follow a turn the user just
39
+ // stopped.
40
+
41
+ import { isValidNativeThreadId } from './native-thread-id.js'
42
+ import type { QueuedThreadTurn } from './thread-turn-queue.js'
43
+
44
+ export interface CursorStopFacts {
45
+ conversationId: string
46
+ status: string
47
+ loopCount: number | null
48
+ cursorVersion: string
49
+ /** The envelope's `ts`: when the hook started, epoch ms from the same Mac clock. */
50
+ hookStartedAtMs: number | null
51
+ }
52
+
53
+ /** The hook's curl gives up this long after it starts (`--max-time 3`). */
54
+ export const CURSOR_HOOK_WAIT_MS = 3_000
55
+ /**
56
+ * A claim is made only this soon after the hook started, so its reply is written with at
57
+ * least a second to spare before the hook stops listening. Past it the turn stays
58
+ * `waiting` for the next Stop.
59
+ */
60
+ export const CURSOR_STOP_MAX_AGE_MS = 2_000
61
+ /** A stamp this far in the future is a clock that moved, not a hook; no claim. */
62
+ export const CURSOR_STOP_MAX_SKEW_MS = 5_000
63
+
64
+ /**
65
+ * Is the hook that sent this still listening? A socket check cannot answer that (QA,
66
+ * 6.51.0): when the event loop is blocked past the hook's 3 s, curl gives up and closes,
67
+ * but Node reads the request bytes before the close, so the handler sees an open socket,
68
+ * claims the turn, and writes the reply to nobody -- the row reads `delivered` and Cursor
69
+ * never got it. The hook's own start stamp can answer it: everything from this check to
70
+ * the reply is synchronous, so a claim made within 2 s of the hook starting is answered
71
+ * before the hook's 3 s are up.
72
+ */
73
+ export function cursorStopIsFresh(hookStartedAtMs: number | null, now: number, maxAgeMs = CURSOR_STOP_MAX_AGE_MS): boolean {
74
+ if (hookStartedAtMs === null || !Number.isFinite(hookStartedAtMs) || !Number.isFinite(now)) return false
75
+ const age = now - hookStartedAtMs
76
+ return age <= maxAgeMs && age >= -CURSOR_STOP_MAX_SKEW_MS
77
+ }
78
+
79
+ /**
80
+ * The spool envelope the hook posts (`{"ts","ppid","event","payload"}`), read strictly.
81
+ * Null unless it is a Stop from Cursor naming a valid conversation.
82
+ */
83
+ export function parseCursorStopEnvelope(body: unknown): CursorStopFacts | null {
84
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null
85
+ const envelope = body as Record<string, unknown>
86
+ if (envelope.event !== 'Stop') return null
87
+ const payload = envelope.payload
88
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null
89
+ const p = payload as Record<string, unknown>
90
+ // Cursor stamps every hook payload with its version. Claude Code never does, so this
91
+ // is the positive evidence the event came from Cursor and not a Claude Stop.
92
+ if (typeof p.cursor_version !== 'string' || p.cursor_version.trim().length === 0) return null
93
+ const raw = typeof p.conversation_id === 'string' ? p.conversation_id : typeof p.session_id === 'string' ? p.session_id : ''
94
+ const conversationId = raw.trim().toLowerCase()
95
+ if (!isValidNativeThreadId(conversationId)) return null
96
+ const status = typeof p.status === 'string' ? p.status : ''
97
+ const loopCount = typeof p.loop_count === 'number' && Number.isFinite(p.loop_count) ? p.loop_count : null
98
+ const hookStartedAtMs = typeof envelope.ts === 'number' && Number.isFinite(envelope.ts) ? envelope.ts : null
99
+ return { conversationId, status, loopCount, cursorVersion: p.cursor_version.trim(), hookStartedAtMs }
100
+ }
101
+
102
+ /**
103
+ * The next waiting turn, claimed. Pure: returns the new queue and the claimed turn, or
104
+ * null when there is nothing to hand over. Queue order, oldest first.
105
+ */
106
+ export function claimNextCursorTurn(
107
+ queue: readonly QueuedThreadTurn[],
108
+ now: number,
109
+ ): { queue: QueuedThreadTurn[]; turn: QueuedThreadTurn } | null {
110
+ const index = queue.findIndex(t => t.status === 'waiting')
111
+ if (index < 0) return null
112
+ const turn: QueuedThreadTurn = {
113
+ ...queue[index]!,
114
+ status: 'delivered',
115
+ attempts: queue[index]!.attempts + 1,
116
+ reason: 'cursor_stop_hook',
117
+ settledAt: now,
118
+ }
119
+ const next = queue.slice()
120
+ next[index] = turn
121
+ return { queue: next, turn }
122
+ }
123
+
124
+ /**
125
+ * Should this Stop receive a queued turn at all? Only a turn that COMPLETED: Cursor
126
+ * reports `aborted` when the user stops it and `error` when it failed, and neither is a
127
+ * moment to add a sentence. A loop count past the queue's own size means something other
128
+ * than this queue is driving the composer.
129
+ */
130
+ export function cursorStopAcceptsFollowup(facts: CursorStopFacts, maxLoop: number): boolean {
131
+ if (facts.status !== 'completed') return false
132
+ if (facts.loopCount !== null && facts.loopCount >= maxLoop) return false
133
+ return true
134
+ }