@gotcos/glasses-server 6.27.5 → 6.27.6
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 +2 -1
- package/package.json +1 -1
- package/server/index.ts +2 -1
- package/server/lib/video-upload-v2.ts +27 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 6.27.6
|
|
2
|
+
- **V2 original chunks are 1 MiB.** Same sequential one-in-flight loop, same
|
|
3
|
+
ArrayBuffer bodies, same GET-progress resume. A 244 MB clip goes from 953
|
|
4
|
+
round trips to ~239. That is the leftover ~10% against legacy 8 MiB, paid as
|
|
5
|
+
per-chunk RTT, without opening a second fetch — two concurrent ArrayBuffer
|
|
6
|
+
PUTs from this WebView are still an untested shape.
|
|
7
|
+
- **In-flight 256 KiB drafts keep their size.** `putOriginal` checks the
|
|
8
|
+
session's own `chunkBytes`, not the live constant, so a draft that started
|
|
9
|
+
before this upgrade still accepts 256 KiB parts and rejects a 1 MiB PUT into
|
|
10
|
+
that slot. New inits advertise 1 MiB. Do not raise this above 1 MiB until the
|
|
11
|
+
phone parser cap is raised first — above that, V2 capability parse returns
|
|
12
|
+
null and the transport silently falls back to legacy.
|
|
13
|
+
- Frame parts stay 256 KiB. Protocol stays 1.
|
|
14
|
+
|
|
1
15
|
## 6.27.5
|
|
2
16
|
- **Chunk uploads now leave a server-side trace.** On 2026-08-12 a phone upload
|
|
3
17
|
stalled on both media transports and the server was a complete blind spot: nothing
|
package/README.md
CHANGED
|
@@ -180,7 +180,8 @@ machine-wide rollback for build 204+ server-owned query recovery),
|
|
|
180
180
|
`COS_MEDIA_ROOT` (optional image/video store location; default
|
|
181
181
|
`~/.cos-glasses/data/media`), and `COS_VIDEO_UPLOAD_V2=1` (private 6.27.3+
|
|
182
182
|
resumable-video canary, managed by COS Control 0.5.20). The V2 canary retains
|
|
183
|
-
accepted
|
|
183
|
+
accepted original chunks (1 MiB on new sessions; leftover 256 KiB drafts keep
|
|
184
|
+
that size) and finalize receipts across restarts; keep it off when
|
|
184
185
|
using an older companion. Your name + transcription vocabulary live in
|
|
185
186
|
`~/.cos-glasses/.cos-profile.json` (see `.cos-profile.example.json`).
|
|
186
187
|
Factory example values are ignored; add the real names, companies, acronyms,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.27.
|
|
3
|
+
"version": "6.27.6",
|
|
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/index.ts
CHANGED
|
@@ -162,7 +162,8 @@ app.use('/api', requireApiToken(API_TOKEN))
|
|
|
162
162
|
// through their true terminal boundary.
|
|
163
163
|
app.use('/api', (req, res, next) => {
|
|
164
164
|
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next()
|
|
165
|
-
// V2 video chunks are bounded to
|
|
165
|
+
// V2 video original chunks are bounded to the advertised session size (1 MiB
|
|
166
|
+
// on new sessions, 256 KiB on leftover drafts) and commit through the upload
|
|
166
167
|
// registry's own generation lock. Holding the global mutation lease while
|
|
167
168
|
// the phone transfers the body recreated the exact 90-second drain failure
|
|
168
169
|
// this protocol exists to remove. Admission is checked by the route before
|
|
@@ -27,7 +27,12 @@ import { getMediaStore } from './media-store.js'
|
|
|
27
27
|
import { MAX_CHUNKED_MEDIA_BYTES, MAX_VIDEO_DURATION_MS } from './rich-media-safety.js'
|
|
28
28
|
|
|
29
29
|
export const VIDEO_UPLOAD_V2_PROTOCOL = 1
|
|
30
|
-
|
|
30
|
+
/** New sessions only. In-flight drafts keep the chunkBytes baked into their
|
|
31
|
+
* manifest — a 256 KiB upload that survives this upgrade must not be rewritten
|
|
32
|
+
* to 1 MiB mid-transfer. The phone parser currently rejects advertised sizes
|
|
33
|
+
* above 1 MiB and disables V2 entirely, so do not raise this without raising
|
|
34
|
+
* that cap first. */
|
|
35
|
+
export const VIDEO_UPLOAD_V2_CHUNK_BYTES = 1024 * 1024
|
|
31
36
|
export const VIDEO_UPLOAD_V2_MAX_FRAME_BYTES = 256 * 1024
|
|
32
37
|
export const VIDEO_UPLOAD_V2_MAX_FRAME_PACK_BYTES = 2 * 1024 * 1024
|
|
33
38
|
export const VIDEO_UPLOAD_PHONE_FRAMES_MIN = 8
|
|
@@ -177,6 +182,17 @@ function parseIndex(value: unknown): number | null {
|
|
|
177
182
|
return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : null
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
/** Exact byte length this session expects for original index `index`.
|
|
186
|
+
* Non-final parts are the session's own chunkBytes (which may be 256 KiB on a
|
|
187
|
+
* draft that started before the 1 MiB advertisement). The last part is the
|
|
188
|
+
* remainder. Using the live constant here would accept a 1 MiB PUT into a
|
|
189
|
+
* 256 KiB slot and fail assembly, or reject a legitimate leftover last chunk. */
|
|
190
|
+
function expectedOriginalPartBytes(manifest: VideoUploadManifest, index: number): number {
|
|
191
|
+
if (index < 0 || index >= manifest.chunkCount) return 0
|
|
192
|
+
if (index === manifest.chunkCount - 1) return manifest.totalBytes - index * manifest.chunkBytes
|
|
193
|
+
return manifest.chunkBytes
|
|
194
|
+
}
|
|
195
|
+
|
|
180
196
|
function sameInit(manifest: VideoUploadManifest, input: Required<Pick<VideoUploadManifest,
|
|
181
197
|
'serverInstanceId' | 'totalBytes' | 'mime'>> & Pick<VideoUploadManifest, 'label' | 'capturedAt' | 'sessionId'>): boolean {
|
|
182
198
|
return manifest.serverInstanceId === input.serverInstanceId
|
|
@@ -442,8 +458,8 @@ export class VideoUploadRegistry {
|
|
|
442
458
|
): Promise<VideoUploadProgress> {
|
|
443
459
|
const index = parseIndex(indexValue)
|
|
444
460
|
if (index === null || bytes.length === 0) throw new VideoUploadError('video_upload_invalid', 'valid non-empty part required')
|
|
445
|
-
const
|
|
446
|
-
if (bytes.length >
|
|
461
|
+
const advertisedMax = kind === 'original' ? VIDEO_UPLOAD_V2_CHUNK_BYTES : VIDEO_UPLOAD_V2_MAX_FRAME_BYTES
|
|
462
|
+
if (bytes.length > advertisedMax) throw new VideoUploadError('video_upload_invalid', 'part exceeds its byte ceiling', { maxBytes: advertisedMax })
|
|
447
463
|
this.activeWriters.set(uploadId, (this.activeWriters.get(uploadId) ?? 0) + 1)
|
|
448
464
|
try {
|
|
449
465
|
return await this.withLock(uploadId, () => {
|
|
@@ -451,6 +467,14 @@ export class VideoUploadRegistry {
|
|
|
451
467
|
if (manifest.state !== 'receiving') throw new VideoUploadError('video_upload_busy', `upload is ${manifest.state}`)
|
|
452
468
|
if (kind === 'original' && index >= manifest.chunkCount) throw new VideoUploadError('video_upload_invalid', 'chunk index exceeds declared upload')
|
|
453
469
|
if (kind === 'frames' && index >= VIDEO_UPLOAD_PHONE_FRAMES_MAX) throw new VideoUploadError('video_upload_invalid', 'frame index exceeds pack limit')
|
|
470
|
+
if (kind === 'original') {
|
|
471
|
+
const expected = expectedOriginalPartBytes(manifest, index)
|
|
472
|
+
if (bytes.length !== expected) {
|
|
473
|
+
throw new VideoUploadError('video_upload_invalid', 'part does not match the session chunk size', {
|
|
474
|
+
expectedBytes: expected, receivedBytes: bytes.length, chunkBytes: manifest.chunkBytes,
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
}
|
|
454
478
|
const collection = manifest[kind]
|
|
455
479
|
const key = String(index)
|
|
456
480
|
const digest = sha256(bytes)
|