@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.
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
482
|
+
let recentTurns: RecentTurnsRead | null = null
|
|
483
483
|
if (turnsWanted !== null) {
|
|
484
484
|
try {
|
|
485
485
|
recentTurns = await readRecentSessionTurns(provider, found, turnsWanted)
|