@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.
- package/.env.example +16 -0
- package/CHANGELOG.md +86 -0
- package/README.md +56 -3
- package/managed-runtime-contract.json +1 -0
- package/package.json +1 -1
- package/server/index.ts +58 -9
- 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-instance-claim.ts +22 -1
- package/server/lib/client-liveness.ts +50 -0
- package/server/lib/codex-bridge.ts +6 -0
- package/server/lib/cursor-bridge.ts +6 -0
- package/server/lib/job-trail.ts +758 -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 +629 -18
- package/server/lib/session-stream-producer.ts +25 -6
- package/server/lib/session-transcript-watcher.ts +120 -3
- package/server/routes/agent-sessions.ts +12 -1
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +14 -7
- package/server/routes/health.ts +24 -1
- package/server/routes/permission-broker.ts +176 -0
- package/server/routes/query-jobs.ts +84 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Router, type Response } from 'express'
|
|
1
|
+
import { Router, type Request, type Response } from 'express'
|
|
2
2
|
import { durableQueryJobsEnabled } from '../lib/query-job-feature.js'
|
|
3
3
|
import {
|
|
4
4
|
QueryJobCoordinator,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
QueryJobPersistenceError,
|
|
17
17
|
QueryJobProviderOrphanFenceError,
|
|
18
18
|
QueryJobStoreError,
|
|
19
|
+
type QueryJobTrailFrame,
|
|
19
20
|
} from '../lib/query-job-store.js'
|
|
20
21
|
import {
|
|
21
22
|
isTerminalQueryJobStatus,
|
|
@@ -89,7 +90,25 @@ function writeSse(res: Response, type: string, event: Record<string, unknown>):
|
|
|
89
90
|
res.write(`event: ${type}\ndata: ${JSON.stringify(event)}\n\n`)
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
/**
|
|
94
|
+
* THE TRAIL IS OPT-IN (6.52.0 QA round 1). A client that does not send `?trail=1` gets
|
|
95
|
+
* exactly the 6.51.0 bytes: no `trail` frames on the stream, and no `trail` field on any
|
|
96
|
+
* snapshot, in any response. A client that does gets `trail` frames (their own dense
|
|
97
|
+
* `trailSeq`, no `id:` line, so Last-Event-ID stays the job eventSeq) and, on subscribe or
|
|
98
|
+
* reconnect, every entry after its `trailAfter` from `snapshot.trail`, or one
|
|
99
|
+
* `trail_snapshot` frame to rebuild from when entries it missed are no longer held.
|
|
100
|
+
*/
|
|
101
|
+
function wantsTrail(req: Request): boolean {
|
|
102
|
+
return req.query.trail === '1'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function forClient(job: QueryJobSnapshot, trail: boolean): QueryJobSnapshot {
|
|
106
|
+
if (trail || !('trail' in job)) return job
|
|
107
|
+
const { trail: _trail, ...rest } = job
|
|
108
|
+
return rest as QueryJobSnapshot
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function snapshotEvent(job: QueryJobSnapshot, reason: string, trail: boolean): Record<string, unknown> {
|
|
93
112
|
return {
|
|
94
113
|
type: 'snapshot',
|
|
95
114
|
eventSeq: job.eventSeq,
|
|
@@ -98,7 +117,7 @@ function snapshotEvent(job: QueryJobSnapshot, reason: string): Record<string, un
|
|
|
98
117
|
generation: job.generation,
|
|
99
118
|
status: job.status,
|
|
100
119
|
at: job.updatedAt,
|
|
101
|
-
data: { reason, job },
|
|
120
|
+
data: { reason, job: forClient(job, trail) },
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -122,7 +141,7 @@ export function createQueryJobsRouter(
|
|
|
122
141
|
try {
|
|
123
142
|
const prepared = options.prepareAdmission ? await options.prepareAdmission(req.body) : req.body
|
|
124
143
|
const admission = await coordinator.submit(prepared)
|
|
125
|
-
return res.status(202).json({ job: admission.job })
|
|
144
|
+
return res.status(202).json({ job: forClient(admission.job, wantsTrail(req)) })
|
|
126
145
|
} catch (error) {
|
|
127
146
|
const wire = wireError(error)
|
|
128
147
|
return res.status(wire.status).json(wire.body)
|
|
@@ -138,7 +157,7 @@ export function createQueryJobsRouter(
|
|
|
138
157
|
const generation = requiredGeneration(req.query.generation)
|
|
139
158
|
const job = await coordinator.getByClientGeneration(clientJobId, generation)
|
|
140
159
|
if (!job) throw new QueryJobNotFoundError(`${clientJobId}:${generation}`)
|
|
141
|
-
return res.json({ job })
|
|
160
|
+
return res.json({ job: forClient(job, wantsTrail(req)) })
|
|
142
161
|
} catch (error) {
|
|
143
162
|
const wire = wireError(error)
|
|
144
163
|
return res.status(wire.status).json(wire.body)
|
|
@@ -150,7 +169,7 @@ export function createQueryJobsRouter(
|
|
|
150
169
|
const jobId = validJobId(req.params.jobId)
|
|
151
170
|
const generation = requiredGeneration(req.query.generation)
|
|
152
171
|
const job = await coordinator.getSnapshot(jobId, generation)
|
|
153
|
-
return res.json({ job })
|
|
172
|
+
return res.json({ job: forClient(job, wantsTrail(req)) })
|
|
154
173
|
} catch (error) {
|
|
155
174
|
const wire = wireError(error)
|
|
156
175
|
return res.status(wire.status).json(wire.body)
|
|
@@ -163,7 +182,15 @@ export function createQueryJobsRouter(
|
|
|
163
182
|
let closed = false
|
|
164
183
|
let streamReady = false
|
|
165
184
|
let sentSeq = -1
|
|
185
|
+
let sentTrailSeq = 0
|
|
166
186
|
const pendingLive: QueryJobEvent[] = []
|
|
187
|
+
const pendingTrail: QueryJobTrailFrame[] = []
|
|
188
|
+
const trail = wantsTrail(req)
|
|
189
|
+
const writeTrail = (frame: QueryJobTrailFrame) => {
|
|
190
|
+
if (closed || frame.trailSeq <= sentTrailSeq) return
|
|
191
|
+
sentTrailSeq = frame.trailSeq
|
|
192
|
+
writeSse(res, 'trail', frame as unknown as Record<string, unknown>)
|
|
193
|
+
}
|
|
167
194
|
const cleanup = () => {
|
|
168
195
|
if (closed) return
|
|
169
196
|
closed = true
|
|
@@ -184,8 +211,22 @@ export function createQueryJobsRouter(
|
|
|
184
211
|
const generation = requiredGeneration(req.query.generation)
|
|
185
212
|
const after = cursor(req.query.after)
|
|
186
213
|
sentSeq = after
|
|
214
|
+
const trailAfter = trail ? cursor(req.query.trailAfter) : 0
|
|
215
|
+
sentTrailSeq = trailAfter
|
|
216
|
+
const trailSubscription = trail
|
|
217
|
+
? {
|
|
218
|
+
after: trailAfter,
|
|
219
|
+
listener: (frame: QueryJobTrailFrame) => {
|
|
220
|
+
if (!streamReady) {
|
|
221
|
+
pendingTrail.push(frame)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
writeTrail(frame)
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
: undefined
|
|
187
228
|
|
|
188
|
-
const
|
|
229
|
+
const onEvent = (event: QueryJobEvent) => {
|
|
189
230
|
// subscribe() registers its live listener before returning the replay
|
|
190
231
|
// snapshot so no append can fall into a gap. A very fast provider may
|
|
191
232
|
// therefore publish while this route is still installing SSE headers.
|
|
@@ -199,7 +240,11 @@ export function createQueryJobsRouter(
|
|
|
199
240
|
sentSeq = event.eventSeq
|
|
200
241
|
writeSse(res, event.type, event as unknown as Record<string, unknown>)
|
|
201
242
|
if (isTerminalQueryJobStatus(event.status)) finish()
|
|
202
|
-
}
|
|
243
|
+
}
|
|
244
|
+
// Without `?trail=1` the call is exactly the 6.51.0 one.
|
|
245
|
+
const subscription = trailSubscription
|
|
246
|
+
? await coordinator.subscribe(jobId, generation, after, onEvent, trailSubscription)
|
|
247
|
+
: await coordinator.subscribe(jobId, generation, after, onEvent)
|
|
203
248
|
unsubscribe = subscription.unsubscribe
|
|
204
249
|
if (closed) {
|
|
205
250
|
unsubscribe()
|
|
@@ -216,9 +261,33 @@ export function createQueryJobsRouter(
|
|
|
216
261
|
res.flushHeaders()
|
|
217
262
|
res.write(': keepalive\n\n')
|
|
218
263
|
|
|
219
|
-
const { replay } = subscription
|
|
264
|
+
const { replay, trailReplay } = subscription
|
|
265
|
+
// The trail first: every entry precedes the terminal event that may end this stream.
|
|
266
|
+
if (trail && trailReplay) {
|
|
267
|
+
if (trailReplay.gap) {
|
|
268
|
+
writeSse(res, 'trail_snapshot', {
|
|
269
|
+
type: 'trail_snapshot',
|
|
270
|
+
reason: trailReplay.reason ?? 'trail_gap',
|
|
271
|
+
jobId: replay.snapshot.jobId,
|
|
272
|
+
clientJobId: replay.snapshot.clientJobId,
|
|
273
|
+
generation: replay.snapshot.generation,
|
|
274
|
+
oldestTrailSeq: trailReplay.oldestTrailSeq,
|
|
275
|
+
latestTrailSeq: trailReplay.latestTrailSeq,
|
|
276
|
+
trail: trailReplay.entries,
|
|
277
|
+
})
|
|
278
|
+
sentTrailSeq = trailReplay.latestTrailSeq
|
|
279
|
+
} else {
|
|
280
|
+
for (const entry of trailReplay.entries) {
|
|
281
|
+
const { trailSeq, at, ...data } = entry
|
|
282
|
+
writeTrail({
|
|
283
|
+
type: 'trail', trailSeq, at, data,
|
|
284
|
+
jobId: replay.snapshot.jobId, clientJobId: replay.snapshot.clientJobId, generation: replay.snapshot.generation,
|
|
285
|
+
} as QueryJobTrailFrame)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
220
289
|
if (replay.gap) {
|
|
221
|
-
writeSse(res, 'snapshot', snapshotEvent(replay.snapshot, replay.reason ?? 'replay_gap'))
|
|
290
|
+
writeSse(res, 'snapshot', snapshotEvent(replay.snapshot, replay.reason ?? 'replay_gap', trail))
|
|
222
291
|
sentSeq = replay.snapshot.eventSeq
|
|
223
292
|
} else {
|
|
224
293
|
for (const event of replay.events) {
|
|
@@ -230,7 +299,7 @@ export function createQueryJobsRouter(
|
|
|
230
299
|
|
|
231
300
|
const replayTerminal = replay.events.some(event => isTerminalQueryJobStatus(event.status))
|
|
232
301
|
if (isTerminalQueryJobStatus(replay.snapshot.status) && !replayTerminal && !replay.gap) {
|
|
233
|
-
writeSse(res, 'snapshot', snapshotEvent(replay.snapshot, 'terminal_snapshot'))
|
|
302
|
+
writeSse(res, 'snapshot', snapshotEvent(replay.snapshot, 'terminal_snapshot', trail))
|
|
234
303
|
sentSeq = replay.snapshot.eventSeq
|
|
235
304
|
}
|
|
236
305
|
if (isTerminalQueryJobStatus(replay.snapshot.status)) {
|
|
@@ -239,6 +308,8 @@ export function createQueryJobsRouter(
|
|
|
239
308
|
}
|
|
240
309
|
|
|
241
310
|
streamReady = true
|
|
311
|
+
for (const frame of pendingTrail) writeTrail(frame)
|
|
312
|
+
pendingTrail.length = 0
|
|
242
313
|
for (const event of pendingLive) {
|
|
243
314
|
if (closed || event.eventSeq <= sentSeq) continue
|
|
244
315
|
sentSeq = event.eventSeq
|
|
@@ -271,7 +342,7 @@ export function createQueryJobsRouter(
|
|
|
271
342
|
const jobId = validJobId(req.params.jobId)
|
|
272
343
|
const generation = requiredGeneration(req.body?.generation)
|
|
273
344
|
const { job } = await coordinator.cancel(jobId, generation)
|
|
274
|
-
return res.json({ job })
|
|
345
|
+
return res.json({ job: forClient(job, wantsTrail(req)) })
|
|
275
346
|
} catch (error) {
|
|
276
347
|
const wire = wireError(error)
|
|
277
348
|
return res.status(wire.status).json(wire.body)
|
|
@@ -283,7 +354,7 @@ export function createQueryJobsRouter(
|
|
|
283
354
|
const jobId = validJobId(req.params.jobId)
|
|
284
355
|
const generation = requiredGeneration(req.body?.generation)
|
|
285
356
|
const job = await coordinator.acknowledge(jobId, generation)
|
|
286
|
-
return res.json({ job })
|
|
357
|
+
return res.json({ job: forClient(job, wantsTrail(req)) })
|
|
287
358
|
} catch (error) {
|
|
288
359
|
const wire = wireError(error)
|
|
289
360
|
return res.status(wire.status).json(wire.body)
|