@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.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -4
- package/package.json +1 -1
- package/server/index.ts +70 -7
- package/server/lib/claude-session-registry.ts +19 -0
- package/server/lib/occupancy-probes.ts +43 -0
- package/server/lib/session-hook-events.ts +5 -0
- package/server/lib/session-hook-ledger.ts +7 -3
- package/server/lib/session-hook-spool.ts +11 -3
- package/server/lib/session-hooks-runtime.ts +114 -5
- package/server/lib/session-signal-store.ts +33 -5
- package/server/lib/session-state-derive.ts +38 -0
- package/server/lib/session-stream-bus.ts +93 -9
- package/server/lib/session-stream-events.ts +144 -0
- package/server/lib/thread-drain-kick.ts +153 -0
- package/server/lib/thread-occupancy.ts +36 -1
- package/server/lib/thread-turn-queue-deliver.ts +17 -6
- package/server/lib/thread-turn-queue-store.ts +70 -23
- package/server/lib/thread-turn-queue.ts +19 -3
- package/server/routes/agent-session-stream.ts +169 -11
- package/server/routes/agent-sessions.ts +34 -5
- package/server/routes/claude-sessions.ts +11 -17
- package/server/routes/session-hooks.ts +10 -0
- package/server/routes/thread-turn-queue.ts +15 -0
|
@@ -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 })
|