@gotcos/glasses-server 6.50.0 → 6.50.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 CHANGED
@@ -1,3 +1,13 @@
1
+ ## 6.50.1
2
+
3
+ What the QA pass on 6.50.0 found in the history it serves (6.50.0 was published before the pass finished; nothing it changed regressed, and a client that does not ask for `?turns` sees no difference).
4
+
5
+ - **Another agent's message is not the user.** A peer-inbox frame ("Another Claude session sent a message: …") is the user's words only when it is a live Continue (`origin.kind: 'peer'`). Teammate reports, idle notifications and other sessions' messages use the same frame on a plain user row with no origin; 20 of them in one real session were served as `role: 'user'` and would have read "YOU" on the lens. They are left out now.
6
+ - **An injected user row never replaces the prompt on the live stream.** An image attachment's `[Image: source: …]`, a skill's "Base directory for this skill" and a compaction summary are written as `isMeta` / `isCompactSummary` user rows right after the real prompt, and each replaced it on the lens's context line (23 of 91 prompt changes in one session). They emit no prompt now; a live Continue (also `isMeta`) still does. This is a fix to the stream's prompt line since 6.48.x, and it lets the glasses match the current turn when they cut the history.
7
+ - **User rows no person typed are left out too:** Claude's `[Request interrupted by user]` marker and the `# AGENTS.md instructions for …` block Codex writes as a user message.
8
+ - **`?turns` above the maximum is clamped to 40**, not refused: a client asking for more gets the most this server serves instead of looking like an older server.
9
+ - Tests: the teammate frame and a plain other-session frame dropped, an interrupted tool row with text dropped, the injected rows emit no prompt while a Continue and a real prompt do, the clamp, and a route test where the history read throws (the page answers 200 without the fields; QA found that promise untested). Mutation gate: four new cases.
10
+
1
11
  ## 6.50.0
2
12
 
3
13
  Scroll back through a running session's conversation on the glasses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.50.0",
3
+ "version": "6.50.1",
4
4
  "description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,8 +66,17 @@ function cursorQuery(text: string): string {
66
66
  return start >= 0 && end > start ? text.slice(start + '<user_query>'.length, end) : text
67
67
  }
68
68
 
69
+ /**
70
+ * User rows that no person typed (6.50.1, QA on 6.50.0): Claude's interrupt marker
71
+ * ("[Request interrupted by user]", 7 in one session) and the AGENTS.md block Codex
72
+ * writes as a user message (a 4,000-character "# AGENTS.md instructions for …" row in
73
+ * 3 of the last 60 rollouts). Each read as "YOU" on the lens.
74
+ */
75
+ const NOT_TYPED_BY_THE_USER = /^\s*(?:\[Request interrupted by user|# AGENTS\.md instructions\b)/
76
+
69
77
  function shaped(role: SessionTurn['role'], raw: string, at: string | undefined): SessionTurn | null {
70
78
  if (isWrapperPrompt(raw)) return null
79
+ if (role === 'user' && NOT_TYPED_BY_THE_USER.test(raw)) return null
71
80
  const text = latestAssistantReply(raw)
72
81
  if (!text) return null
73
82
  return at ? { role, text, at } : { role, text }
@@ -88,8 +97,13 @@ export function sessionTurnFromRecord(provider: AgentProvider, obj: Record<strin
88
97
  if (!text) return null
89
98
  const at = iso(obj.timestamp)
90
99
  if (obj.type === 'assistant') return shaped('assistant', text, at)
91
- const peer = (obj.origin as { kind?: unknown } | undefined)?.kind === 'peer' ? unwrapPeerMessage(text) : null
92
- if (peer !== null) return shaped('user', peer, at)
100
+ // A peer-inbox message. Only a live Continue (`origin.kind: 'peer'`, 6.49.x) is the
101
+ // user's own words; every other message in that frame is another agent talking
102
+ // (a teammate's report, an idle notification, another session's SendMessage),
103
+ // written as a plain user row with no origin. QA found 20 of those in one session
104
+ // rendering as "YOU".
105
+ const peer = unwrapPeerMessage(text)
106
+ if (peer !== null) return (obj.origin as { kind?: unknown } | undefined)?.kind === 'peer' ? shaped('user', peer, at) : null
93
107
  if (obj.isMeta === true || obj.isCompactSummary === true) return null
94
108
  return shaped('user', text, at)
95
109
  }
@@ -125,7 +139,7 @@ export interface RecentTurnsRead {
125
139
  turns: SessionTurn[]
126
140
  /** True when the transcript holds messages before the first one returned. */
127
141
  more: boolean
128
- /** Bytes of the transcript this read covered, for the log. */
142
+ /** Bytes of the transcript this read covered (tests assert the window growth by it). */
129
143
  bytesRead: number
130
144
  }
131
145
 
@@ -170,9 +184,12 @@ export async function readRecentSessionTurns(
170
184
  }
171
185
  }
172
186
 
173
- /** `?turns=N`: an integer 1..SESSION_TURNS_MAX, or null (absent or malformed). */
187
+ /** `?turns=N`: an integer, clamped to 1..SESSION_TURNS_MAX; null when absent, zero or
188
+ * malformed. Clamped rather than refused above the max, so a client asking for more
189
+ * than this server serves gets the most it can have instead of looking like an older
190
+ * server with no history at all. */
174
191
  export function parseTurnsParam(value: unknown): number | null {
175
192
  if (typeof value !== 'string' || !/^\d{1,3}$/.test(value)) return null
176
193
  const n = Number(value)
177
- return n >= 1 && n <= SESSION_TURNS_MAX ? n : null
194
+ return n >= 1 ? Math.min(n, SESSION_TURNS_MAX) : null
178
195
  }
@@ -559,7 +559,16 @@ function draftsFromClaudeRecord(record: Record<string, unknown>): SessionStreamD
559
559
  const message = asRecord(record.message)
560
560
  if (!message) return []
561
561
  const outcomes = outcomeDrafts(record, message)
562
- return outcomes.length > 0 ? outcomes : promptDrafts(message)
562
+ if (outcomes.length > 0) return outcomes
563
+ // An INJECTED user row is not the query (6.50.0): an image attachment's
564
+ // "[Image: source: …]", a skill's "Base directory for this skill", a compaction
565
+ // summary. Each is written as `isMeta` / `isCompactSummary` right after the real
566
+ // prompt and used to replace it on the lens's context line (23 of 91 prompt changes
567
+ // in one real session). A live Continue is the exception: it is `isMeta` and IS
568
+ // the user's words.
569
+ const liveContinue = (record.origin as { kind?: unknown } | undefined)?.kind === 'peer'
570
+ if ((record.isMeta === true || record.isCompactSummary === true) && !liveContinue) return []
571
+ return promptDrafts(message)
563
572
  }
564
573
 
565
574
  const role = typeof record.role === 'string' ? record.role : ''
@@ -61,7 +61,7 @@ import {
61
61
  type LockHolderSnapshot,
62
62
  } from '../lib/occupancy-probes.js'
63
63
  import type { OccupancyDirs } from '../lib/thread-occupancy.js'
64
- import { parseTurnsParam, readRecentSessionTurns } from '../lib/agent-session-turns.js'
64
+ import { parseTurnsParam, readRecentSessionTurns, type RecentTurnsRead } from '../lib/agent-session-turns.js'
65
65
  import { cosSpawnedPids } from '../lib/agent-session-ownership-store.js'
66
66
 
67
67
  export const agentSessionsRouter = Router()
@@ -479,7 +479,7 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
479
479
  // when asked, so the detail poll and every older client get the same bytes as before.
480
480
  // A failure here costs the history, never the detail page.
481
481
  const turnsWanted = parseTurnsParam(req.query.turns)
482
- let recentTurns: Awaited<ReturnType<typeof readRecentSessionTurns>> | null = null
482
+ let recentTurns: RecentTurnsRead | null = null
483
483
  if (turnsWanted !== null) {
484
484
  try {
485
485
  recentTurns = await readRecentSessionTurns(provider, found, turnsWanted)