@gotcos/glasses-server 6.27.4 → 6.27.5
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 +25 -0
- package/package.json +1 -1
- package/server/routes/media.ts +57 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## 6.27.5
|
|
2
|
+
- **Chunk uploads now leave a server-side trace.** On 2026-08-12 a phone upload
|
|
3
|
+
stalled on both media transports and the server was a complete blind spot: nothing
|
|
4
|
+
recorded that a chunk request had arrived, so "the client never got the ack" could
|
|
5
|
+
not be separated from "the server never sent one" without reading a staging file's
|
|
6
|
+
mtime and running `netstat` by hand.
|
|
7
|
+
- `[media-chunk]` now logs one line per REQUEST (never per data event - a 237-chunk
|
|
8
|
+
upload logging per `data` would bury the log): `body-read` when the body finishes
|
|
9
|
+
arriving, `responded` with status when the response is fully flushed to the socket,
|
|
10
|
+
`abandoned` when the client goes away mid-body, and `closed-unanswered` when the
|
|
11
|
+
socket closes with no response written. Each carries bytes received and elapsed ms.
|
|
12
|
+
- `closed-unanswered` is the only case where blaming the server is correct, and it is
|
|
13
|
+
now stated explicitly rather than inferred from absence of evidence. It gates on
|
|
14
|
+
**`res` 'close', not `req` 'close'**: since Node 16 `IncomingMessage` emits 'close'
|
|
15
|
+
when the REQUEST completes rather than when the socket does, so on an async handler
|
|
16
|
+
(`putOriginal` and `putFrame` both are) it lands while `res.writableEnded` is still
|
|
17
|
+
false. Gated on `req` it false-fired on EVERY successful V2 chunk — 237 bogus alarms
|
|
18
|
+
per upload on the single most diagnostic line in the file. Caught by adversarial QA
|
|
19
|
+
before release and pinned by a test that fails if the gate moves back.
|
|
20
|
+
- The two early refusals (`chunk_bytes_required` 400 and declared-Content-Length 413)
|
|
21
|
+
returned BEFORE the tracer existed, so a chunk the server actively rejected logged
|
|
22
|
+
nothing — the same silence as a request that never arrived. The tracer is now
|
|
23
|
+
declared above them and every refusal logs.
|
|
24
|
+
- Diagnosis only. No behaviour change to any upload path.
|
|
25
|
+
|
|
1
26
|
## 6.27.4
|
|
2
27
|
|
|
3
28
|
### Fuzzy name corrections reach phone dictation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.27.
|
|
3
|
+
"version": "6.27.5",
|
|
4
4
|
"description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/routes/media.ts
CHANGED
|
@@ -255,7 +255,42 @@ export function createMediaChunkBodyParser(options: MediaChunkParserOptions = {}
|
|
|
255
255
|
return
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// Chunk-upload lifecycle trace.
|
|
259
|
+
//
|
|
260
|
+
// WHY. On 2026-08-12 a phone upload stalled on BOTH transports and the server side
|
|
261
|
+
// was a complete blind spot: nothing recorded that a chunk request had even
|
|
262
|
+
// arrived, so "the client never got the ack" could not be separated from "the
|
|
263
|
+
// server never sent one" without reading a staging file's mtime and running
|
|
264
|
+
// netstat by hand.
|
|
265
|
+
//
|
|
266
|
+
// SAMPLED like the client's breadcrumbs, and for the same reason. One line per
|
|
267
|
+
// REQUEST is still 2 rows x 248 chunks per video (~500), and at the advertised
|
|
268
|
+
// 2 GB ceiling ~16,400 rows and ~1.5 MB of log for ONE upload, into a LaunchAgent
|
|
269
|
+
// stdout with no rotation. Routine progress is sampled; the DIAGNOSTIC events —
|
|
270
|
+
// abandoned, closed-unanswered, too-large, and any non-2xx — are never sampled
|
|
271
|
+
// away, because those are the rows an investigation actually needs.
|
|
272
|
+
//
|
|
273
|
+
// Declared ABOVE the early refusals below: those used to return before this
|
|
274
|
+
// existed, so a chunk the server actively refused logged nothing at all — the
|
|
275
|
+
// same silence as a request that never arrived.
|
|
276
|
+
const openedAt = Date.now()
|
|
277
|
+
// `?? ''` is load-bearing: a request with neither field (the direct-parser test
|
|
278
|
+
// harness, and any non-Express caller) made `path.slice` throw and took the whole
|
|
279
|
+
// parser down. A tracer must never be able to break the thing it observes.
|
|
280
|
+
const path = req.originalUrl?.split('?')[0] ?? req.url ?? ''
|
|
281
|
+
const traceTarget = `${req.method} ${path}`
|
|
282
|
+
// Trailing segment is the chunk index on both routes
|
|
283
|
+
// (`/media/upload/:id/:index` and `/media/video-upload/:id/original/:index`).
|
|
284
|
+
const chunkIndex = Number(path.slice(path.lastIndexOf('/') + 1))
|
|
285
|
+
const sampled = !Number.isFinite(chunkIndex) || chunkIndex === 0 || chunkIndex % 32 === 0
|
|
286
|
+
let received = 0
|
|
287
|
+
const logLifecycle = (event: string, extra = '', always = false): void => {
|
|
288
|
+
if (!always && !sampled) return
|
|
289
|
+
console.log(`[media-chunk] ${event} ${traceTarget} bytes=${received} +${Date.now() - openedAt}ms${extra}`)
|
|
290
|
+
}
|
|
291
|
+
|
|
258
292
|
const refuse = (status: number, body: Record<string, unknown>): void => {
|
|
293
|
+
logLifecycle('refused', ` status=${status} error=${String(body.error ?? '')}`, true)
|
|
259
294
|
res.status(status).json(body)
|
|
260
295
|
res.once('finish', () => req.destroy())
|
|
261
296
|
}
|
|
@@ -279,7 +314,6 @@ export function createMediaChunkBodyParser(options: MediaChunkParserOptions = {}
|
|
|
279
314
|
}
|
|
280
315
|
|
|
281
316
|
const parts: Buffer[] = []
|
|
282
|
-
let received = 0
|
|
283
317
|
let settled = false
|
|
284
318
|
|
|
285
319
|
req.on('data', (chunk: Buffer) => {
|
|
@@ -288,6 +322,7 @@ export function createMediaChunkBodyParser(options: MediaChunkParserOptions = {}
|
|
|
288
322
|
if (received > maxChunkBytes) {
|
|
289
323
|
settled = true
|
|
290
324
|
parts.length = 0
|
|
325
|
+
logLifecycle('too-large', ` max=${maxChunkBytes}`)
|
|
291
326
|
refuse(413, { error: 'attachment_too_large', maxBytes: maxChunkBytes })
|
|
292
327
|
return
|
|
293
328
|
}
|
|
@@ -297,12 +332,33 @@ export function createMediaChunkBodyParser(options: MediaChunkParserOptions = {}
|
|
|
297
332
|
if (settled) return
|
|
298
333
|
settled = true
|
|
299
334
|
parts.length = 0
|
|
335
|
+
// The client went away mid-body. Distinguishes a phone that stopped sending
|
|
336
|
+
// from a server that stopped answering — opposite diagnoses, same silence.
|
|
337
|
+
logLifecycle('abandoned')
|
|
300
338
|
}
|
|
301
339
|
req.once('aborted', abandon)
|
|
302
340
|
req.once('error', abandon)
|
|
341
|
+
// Fires when the response is fully flushed to the socket. Paired with 'body-read'
|
|
342
|
+
// this is the proof that the server answered, and how fast.
|
|
343
|
+
res.once('finish', () => {
|
|
344
|
+
logLifecycle('responded', ` status=${res.statusCode}`)
|
|
345
|
+
})
|
|
346
|
+
// `res` 'close', NOT `req` 'close'. Since Node 16 `IncomingMessage` emits 'close'
|
|
347
|
+
// when the REQUEST completes, not when the socket does — so on any async handler
|
|
348
|
+
// it fires while `res.writableEnded` is still false. `putOriginal`/`putFrame` are
|
|
349
|
+
// both async, so gating on `req` made this false-fire on EVERY successful V2
|
|
350
|
+
// chunk: 237 bogus rows per upload, on the single line the changelog calls "the
|
|
351
|
+
// only case where blaming the server is correct". A watcher that cries wolf on the
|
|
352
|
+
// happy path is worse than no watcher. `res` 'close' fires after the response is
|
|
353
|
+
// finished or genuinely aborted, so the guard means what it says.
|
|
354
|
+
res.once('close', () => {
|
|
355
|
+
if (res.writableEnded) return
|
|
356
|
+
logLifecycle('closed-unanswered', '', true)
|
|
357
|
+
})
|
|
303
358
|
req.once('end', () => {
|
|
304
359
|
if (settled) return
|
|
305
360
|
settled = true
|
|
361
|
+
logLifecycle('body-read')
|
|
306
362
|
chunkBodies.set(req, Buffer.concat(parts))
|
|
307
363
|
next()
|
|
308
364
|
})
|