@gotcos/glasses-server 6.36.5 → 6.36.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 CHANGED
@@ -1,5 +1,31 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 6.36.6
4
+ - **The session digest follows the thread instead of its opening.** Miles, from the
5
+ lens: "It's currently showing a legacy session that I had over a day ago... The
6
+ discussion should show the questions that we're asking and a summary of those most
7
+ recent things, not something that's the 'first' message." The whole budget now goes
8
+ to recency. `DIGEST_HEAD_TURNS` drops from 2 to 0 — a deliberate reversal of the
9
+ earlier rule that reserved the opening ask because it "frames everything after it".
10
+ The opening is not lost: `first_prompt` still carries it in full.
11
+ - **Harness-injected rows no longer render as things you asked.** A slash-command body
12
+ and a compaction preamble are both written as USER rows, so both appeared in the
13
+ DISCUSSION list. Filtered on the STRUCTURAL flags Claude already sets — `isMeta` and
14
+ `isCompactSummary` — not on a markdown heuristic that could misfire on a real paste.
15
+ The compaction preamble is also filtered by text as a fallback for providers that
16
+ emit no flag, and that filter reaches titles and the search index too: the preamble
17
+ is byte-identical across every compacted session, so as a title or a search hit it
18
+ distinguishes nothing.
19
+ - **On a truncated read the head window no longer feeds the recency list.** The head
20
+ window IS the session opening, and on a large session the 60-turn window never fills,
21
+ so those turns survived at the top of the digest indefinitely. Measured on a real
22
+ 94 MiB session: both leading bullets were head-window turns from the previous day.
23
+ A whole-file read is untouched — it yields `tail: true` for every line.
24
+ - Verified by parsing real transcripts, not fixtures: the 94 MiB session now leads with
25
+ the two most recent asks, and an ordinary 2.7 MiB session renders nine recent turns
26
+ in reading order.
27
+
28
+
3
29
  ## 6.36.5
4
30
  - **A speaker merge now reaches the meetings, not just the voice store.**
5
31
  `merge-profiles` folded two profiles together and relabelled the calibration log,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.36.5",
3
+ "version": "6.36.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": {
@@ -103,6 +103,22 @@ export function isKeepWarmSessionTitle(title: string): boolean {
103
103
  return t.startsWith('this is an automated local readiness check')
104
104
  }
105
105
 
106
+ /**
107
+ * The compaction preamble, which the harness writes as a USER turn.
108
+ *
109
+ * Miles's 2026-08-17 screenshot had this sitting in the DISCUSSION list as though he
110
+ * had asked it: "This session is being continued from a previous conversation… Summary:
111
+ * ## 1. Primary Request and Intent". It is scaffolding, and it lands in the digest at
112
+ * full length because it IS recent -- compaction happens mid-session -- so recency
113
+ * ordering alone does not remove it.
114
+ *
115
+ * Measured across the 25 most recently written transcripts on this machine: 29
116
+ * occurrences, exactly ONE distinct opening. Anchored to that opening rather than to a
117
+ * loose "continued from" match, so a human sentence about continuing a conversation is
118
+ * not mistaken for it.
119
+ */
120
+ const COMPACTION_PREAMBLE = /^this session is being continued from a previous conversation/i
121
+
106
122
  export function isWrapperPrompt(text: string): boolean {
107
123
  const trimmed = text.trim()
108
124
  return trimmed.startsWith('<')
@@ -110,6 +126,10 @@ export function isWrapperPrompt(text: string): boolean {
110
126
  || trimmed.startsWith('You are an agent')
111
127
  || trimmed.startsWith('You are QA Agent')
112
128
  || trimmed.startsWith('Message Type:')
129
+ // Filtered HERE rather than only in the digest, so it also stops titling a session
130
+ // and stops being indexed for search. It is byte-identical across every compacted
131
+ // session, so as a title or a search hit it distinguishes nothing.
132
+ || COMPACTION_PREAMBLE.test(trimmed)
113
133
  }
114
134
 
115
135
  export function firstLineTitle(text: string): string {
@@ -249,7 +269,27 @@ export const DISCUSSION_DIGEST_MAX = 2000
249
269
  const DIGEST_TURN_MAX = 220
250
270
 
251
271
  /** Turns kept from the START. The opening ask frames everything after it. */
252
- const DIGEST_HEAD_TURNS = 2
272
+ /**
273
+ * User turns reserved from the START of the session before recency gets the budget.
274
+ *
275
+ * ZERO, AND THAT IS A REVERSAL. This was 2, on the reasoning that the opening ask
276
+ * "frames everything after it" -- see the head-first block in composeDiscussionDigest,
277
+ * which was itself a fix for a digest that spent everything on the tail.
278
+ *
279
+ * Miles overruled it from hardware, 2026-08-17, looking at a digest whose first two
280
+ * bullets were a question from the previous day and a compaction preamble: "It needs to
281
+ * refresh to the most recent response inside of the thread. The discussion should show
282
+ * the questions that we're asking and a summary of those most recent things, not
283
+ * something that's the 'first' message."
284
+ *
285
+ * The opening ask is NOT lost -- it is still carried, in full, in `first_prompt`, which
286
+ * the digest and the row label both fall back to. What changes is that it stops
287
+ * occupying the two most valuable slots in a list about what is happening NOW.
288
+ *
289
+ * Left as a constant rather than deleting the head path: this is a judgement call about
290
+ * emphasis, and one number is the whole reversal if Miles wants some framing back.
291
+ */
292
+ const DIGEST_HEAD_TURNS = 0
253
293
 
254
294
  /** Recent turns retained while streaming. Comfortably more than 2000 chars can
255
295
  * render (~20-25 at typical length), so the budget and not the buffer decides
@@ -1341,7 +1381,32 @@ export async function parseAgentSession(
1341
1381
  // reports what was actually dropped rather than what this buffer happens to hold.
1342
1382
  const digestHead: string[] = []
1343
1383
  const digestRecent: string[] = []
1344
- const collectTurn = (text: string): void => {
1384
+ /**
1385
+ * Add a user turn to the recency digest -- or decline to.
1386
+ *
1387
+ * TWO DECLINES, both found by parsing Miles's own 94 MiB session rather than by
1388
+ * reasoning about the code.
1389
+ *
1390
+ * `injected`: STRUCTURAL, not a heuristic. Claude marks a slash-command body with
1391
+ * `isMeta: true` and a compaction preamble with `isCompactSummary: true`. Both are
1392
+ * written as USER rows, so both rendered in the digest as things Miles had asked --
1393
+ * his screenshot led with "This session is being continued from a previous
1394
+ * conversation… Summary: ## 1. Primary Request and Intent". Neither is an ask, and
1395
+ * both are recent, so recency ordering alone leaves them exactly where they were.
1396
+ *
1397
+ * `fromTail` on a TRUNCATED read: the head window IS the session opening. Feeding it
1398
+ * into a recency list is the very thing Miles objected to -- "not something that's
1399
+ * the 'first' message" -- and on a large session the 60-turn window never fills, so
1400
+ * those opening turns survive to the top of the digest forever. Measured on his
1401
+ * session: the two leading bullets were both head-window turns from the previous day.
1402
+ * The head is still read and still published, as `first_prompt`.
1403
+ *
1404
+ * A whole-file read yields `tail: true` for every line, so a normal session is
1405
+ * unaffected -- this narrows only the partial read.
1406
+ */
1407
+ const collectTurn = (text: string, fromTail = true, injected = false): void => {
1408
+ if (injected) return
1409
+ if (truncated && !fromTail) return
1345
1410
  // Reuse the wrapper filter the rest of this module already trusts. Without it the
1346
1411
  // opening turns of a slash-command session render as
1347
1412
  // "<command-message>cos-glasses</command-message>…" — scaffolding, not the ask,
@@ -1413,7 +1478,7 @@ export async function parseAgentSession(
1413
1478
  }
1414
1479
  if (obj.type === 'user') {
1415
1480
  userCount += 1
1416
- collectTurn(text)
1481
+ collectTurn(text, tail, obj.isMeta === true || obj.isCompactSummary === true)
1417
1482
  if (!firstPrompt) firstPrompt = firstLineTitle(text)
1418
1483
  if (!title) title = firstLineTitle(text)
1419
1484
  } else {
@@ -1459,7 +1524,7 @@ export async function parseAgentSession(
1459
1524
  if (obj.role === 'user') {
1460
1525
  userCount += 1
1461
1526
  const query = cursorUserTitle(text, true) ?? cursorUserTitle(text, false)
1462
- collectTurn(query ?? text)
1527
+ collectTurn(query ?? text, tail)
1463
1528
  if (query) {
1464
1529
  title = query
1465
1530
  if (!firstPrompt) firstPrompt = query