@gotcos/glasses-server 6.28.0 → 6.30.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/CHANGELOG.md CHANGED
@@ -2219,6 +2219,58 @@ unsaved capture, and makes batch status stop lying about finished work.
2219
2219
 
2220
2220
  # Changelog
2221
2221
 
2222
+ ## [6.30.0] - 2026-08-16
2223
+
2224
+ ### Sessions know which threads are live
2225
+
2226
+ - `GET /api/agent-sessions` now stamps each row with `running` (an agent is
2227
+ working in this thread right now) and `running_foreign` (it is held by
2228
+ something that is not COS, so a Continue would be refused), plus a
2229
+ `runningDegraded` flag on the payload when a probe could not see clearly.
2230
+ - `running` counts COS's own queued turn too. Two different questions: whether an
2231
+ agent is working (the badge) and whether a write would be refused (the
2232
+ affordance). Counting only foreign owners would hide your own turn from the
2233
+ screen you open to watch it.
2234
+ - **This is a display hint and never a write gate.** Attach and turn keep probing
2235
+ at the moment of the write, unchanged, so a desktop session opened between the
2236
+ list render and the tap is still caught.
2237
+ - Fails the opposite way to the gate: doubt reports `degraded` rather than
2238
+ painting every session busy.
2239
+ - One scan for the whole page, measured at 7ms for 45 real sessions (was 792ms
2240
+ before a scan-scoped memo and skipping lsof when no Codex writer lock exists).
2241
+
2242
+ **Required by COS Glasses 6.8.364.**
2243
+
2244
+ ## [6.29.0] - 2026-08-16
2245
+
2246
+ ### Continue is queued instead of holding the phone
2247
+
2248
+ - An attached turn is admitted with **202** and delivered in the background. A
2249
+ provider turn runs up to 21 minutes and iOS suspends the WebView the moment the
2250
+ phone is pocketed, so a synchronous turn was lost exactly when the user did the
2251
+ natural thing.
2252
+ - Every safety gate stays synchronous: body, replay, queued-prompt, lease, target,
2253
+ epoch, fence, claim, occupancy, head baseline, pin. A refusal still reaches the
2254
+ user immediately and precisely. Only the spawn moved.
2255
+ - **New:** `GET /api/agent-sessions/bindings/:bindingId/turns/:clientTurnId`
2256
+ serves the same durable ledger record the replay path serves, so a poll and a
2257
+ retry can never disagree about what a turn did. Reports `unknown` rather than
2258
+ `failed` for a key it has never seen.
2259
+ - Fixed: a post-202 refusal recorded nothing, so the status route would have
2260
+ answered `pending` forever. Pre-202 refusals keep the old semantics and stay
2261
+ re-evaluatable.
2262
+
2263
+ ### Fork, ungated
2264
+
2265
+ - `POST /api/agent-sessions/:provider/:threadId/fork` is registered whether or not
2266
+ `COS_THREAD_ATTACH_ENABLED` is set. Fork appends to nothing: the original is left
2267
+ byte-identical. It is what every refusal tells the user to do instead, and a
2268
+ refusal pointing at a route that 404s is a dead end.
2269
+
2270
+ **Required by COS Glasses 6.8.363.** Continue still needs
2271
+ `COS_THREAD_ATTACH_ENABLED=1`; with it unset the write routes are not registered
2272
+ and behavior is unchanged.
2273
+
2222
2274
  ## 6.12.7
2223
2275
 
2224
2276
  Pairs the public server with COS Glasses build 227+ meeting follow-ups while
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.28.0",
3
+ "version": "6.30.0",
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
@@ -26,6 +26,7 @@ import { cosSpawnedPids } from './lib/agent-session-ownership-store.js'
26
26
  import { realOccupancyDirs, realOccupancyProbes } from './lib/occupancy-probes.js'
27
27
  import { realAttachedWorkspaceDeps, resolveAttachedWorkspace } from './lib/attached-workspace.js'
28
28
  import { deliverAttachedTurn, realAttachedTurnDeps } from './lib/attached-provider-adapter.js'
29
+ import { forkThread, realForkDeps } from './lib/fork-thread.js'
29
30
  import { nativeHead, realNativeHeadDeps } from './lib/native-head.js'
30
31
  import { threadOccupancy } from './lib/thread-occupancy.js'
31
32
  import { displayRouter } from './routes/display.js'
@@ -376,6 +377,43 @@ const deliverAttachedTurnForRoute = async (request: {
376
377
  })
377
378
  }
378
379
 
380
+ /**
381
+ * The shim for FORK — the action seventeen refusal strings in this feature already
382
+ * recommend, and which until 6.29 existed nowhere in the server or the app.
383
+ *
384
+ * Deliberately much thinner than the attached shim above, and every difference is
385
+ * load-bearing:
386
+ *
387
+ * - NO PREFLIGHT. `realAttachedTurnDeps` takes an occupancy re-check because it is
388
+ * about to APPEND to a thread someone may have open on their desktop. A fork
389
+ * appends to nothing: verified byte-for-byte on both providers on 2026-08-16,
390
+ * source sha256 unchanged across the run. A live desktop owner must therefore
391
+ * NOT block a fork — gating it here would refuse at exactly the moment fork is
392
+ * the only path the user has left.
393
+ * - NO `onSpawn` HAND-BACK. The turns route owns its own ledger record because it
394
+ * holds the `recordedPids` list that its `finally` releases from. The fork route
395
+ * holds no such list, so `fork-thread.ts` records and releases the child itself,
396
+ * exactly once, on every exit path — which keeps the recorded:released ratio the
397
+ * operator reads for leak detection at 1:1.
398
+ * - A WATERMARK IS SUPPLIED, and it is the point. `nativeHead` of the SOURCE, read
399
+ * once before the spawn and once after the child settles, is what turns "a fork
400
+ * does not touch the original" from a promise into a checked property. It is
401
+ * SYNCHRONOUS (native-head.ts:491), which is what lets the module read the
402
+ * baseline at the spawn boundary without an await. Unwired, the module can only
403
+ * ever report `unverified`, and it would report that honestly rather than
404
+ * claiming a verification it never performed.
405
+ */
406
+ const forkThreadForRoute = (request: {
407
+ provider: 'claude' | 'codex'
408
+ nativeThreadId: string
409
+ prompt: string
410
+ cwd: string
411
+ policy: 'read_only'
412
+ }): Promise<unknown> => forkThread({
413
+ ...request,
414
+ deps: realForkDeps((provider, threadId) => nativeHead(provider, threadId, nativeHeadDeps)),
415
+ })
416
+
379
417
  // API routes
380
418
  app.use('/api', healthRouter)
381
419
  app.use('/api', diagRouter)
@@ -413,6 +451,13 @@ app.use('/api', createAgentSessionBindingsRouter({
413
451
  },
414
452
  nativeHead: (provider, threadId) => nativeHead(provider, threadId, nativeHeadDeps),
415
453
  deliverAttachedTurn: deliverAttachedTurnForRoute as never,
454
+ forkThread: forkThreadForRoute,
455
+ // The fork's real spawn directory. Separate from `resolveTarget` above, which
456
+ // deliberately yields only fingerprints because plan 3.3 keeps a filesystem path
457
+ // off anything client-visible. Null refuses: never fall back to the server's own
458
+ // working directory, which is wherever the LaunchAgent happened to start.
459
+ resolveForkWorkspace: (provider, threadId) =>
460
+ resolveAttachedWorkspace(provider, threadId, attachedWorkspaceDeps)?.path ?? null,
416
461
  // One instance per process. The epoch high-water mark is only monotonic if a
417
462
  // single reader owns the durable store, so this must never be constructed twice.
418
463
  // `open()` never throws: an unreadable store yields a DEGRADED registry whose