@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.
- package/.env.example +16 -0
- package/CHANGELOG.md +78 -0
- package/README.md +56 -3
- package/bin/hooks/cos-session-hook +24 -1
- package/managed-runtime-contract.json +1 -0
- package/package.json +1 -1
- package/server/index.ts +83 -5
- 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-liveness.ts +50 -0
- package/server/lib/codex-bridge.ts +6 -0
- package/server/lib/codex-live-queue-deps.ts +135 -0
- package/server/lib/codex-live-queue.ts +232 -0
- package/server/lib/codex-turn-clock.ts +36 -0
- package/server/lib/cursor-bridge.ts +6 -0
- package/server/lib/cursor-stop-followup.ts +134 -0
- package/server/lib/job-trail.ts +758 -0
- package/server/lib/occupancy-probes.ts +52 -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 +600 -15
- package/server/lib/session-stream-producer.ts +25 -6
- package/server/lib/session-transcript-watcher.ts +120 -3
- package/server/lib/thread-occupancy.ts +30 -7
- package/server/routes/agent-session-bindings.ts +98 -6
- package/server/routes/agent-sessions.ts +24 -2
- package/server/routes/cursor-stop-followup.ts +74 -0
- package/server/routes/health.ts +34 -1
- package/server/routes/permission-broker.ts +176 -0
- package/server/routes/query-jobs.ts +84 -13
- 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
|
+
}
|