@gotcos/glasses-server 6.48.1 → 6.48.2

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,11 @@
1
+ ## 6.48.2
2
+
3
+ Continue and Fork work again on a session whose transcript opens with a big row. Reported from the lens on 6.9.482 (Miles, 2026-09-15): Continue on a Claude session answered "Can't continue. Fork thread instead" while the row said the session was attachable.
4
+
5
+ - The attach route reads the session's working directory out of the transcript's first rows. It read a fixed 512 KiB, and a session whose first prompt carried pasted screenshots opens with one row of about 1 MB, so the window held four bookkeeping rows and a torn fifth, no `cwd`, and every Continue and Fork on that session was refused `target_unresolvable`. The read now widens (512 KiB, 2 MiB, 8 MiB, 16 MiB) while the window keeps ending inside a row with no `cwd` seen, and stops as soon as the whole file has been read or a row boundary shows the transcript records none.
6
+ - The first `cwd` in the transcript is the workspace. Two different values used to refuse the attach as a disagreement; on the release Mac every such transcript was one session whose shell had `cd`-ed into a subfolder inside a turn, and Claude stamps each row with the process cwd of the moment. The directory the session started in keys its project folder and is the only place `--resume` finds it, so it is the answer. On the release Mac 738 of 758 transcripts resolve (before: 703); the 20 that do not are scratch sessions whose temporary directory has since been deleted, which is a refusal by design.
7
+ - Tests: the first-cwd rule for Claude and Codex, the torn-window signal, the widening sequence against the production shape (four bookkeeping rows, a 950 KB image row, then the cwd), a short file read once, a first row past the cap refused at the cap. Mutation gate: the widening and the first-cwd rule.
8
+
1
9
  ## 6.48.1
2
10
 
3
11
  One derivation everywhere, a Desktop turn ends when the engine says it ended, a queued follow-up drains then instead of on the 30 s idle window, and the live feed carries the same state every row does. No client change is required: the next Control and EHPK builds read the new keys, and today's consumers ignore them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.48.1",
3
+ "version": "6.48.2",
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": {
@@ -61,9 +61,20 @@ export interface AttachedWorkspaceDeps {
61
61
  cursorSpawnWorkspace?: (threadId: string) => string | null
62
62
  }
63
63
 
64
- /** Enough to reach a Claude message row or the Codex session meta row. */
64
+ /**
65
+ * The first window read for the cwd: enough to reach a Claude message row or the Codex
66
+ * session meta row in the common case. NOT enough on its own (6.48.2): a session whose
67
+ * first prompt carries pasted screenshots opens with a single row of ~1 MB, so 512 KiB
68
+ * held four bookkeeping rows and a torn fifth, no cwd, and every Continue on that
69
+ * session was refused `target_unresolvable` (Miles, 2026-09-15, from the lens; 55 of
70
+ * 758 transcripts on the release Mac). When a window ends inside a row before any cwd
71
+ * was seen, the read widens up to WORKSPACE_SCAN_BYTES_MAX.
72
+ */
65
73
  export const WORKSPACE_SCAN_BYTES = 512 * 1024
66
74
 
75
+ /** The widest head read: a first prompt with ten screenshots fits; a 13 GB rollout does not get read. */
76
+ export const WORKSPACE_SCAN_BYTES_MAX = 16 * 1024 * 1024
77
+
67
78
  /** Rows to inspect before giving up. Bounds a pathological single-line file. */
68
79
  export const WORKSPACE_SCAN_ROWS = 400
69
80
 
@@ -72,19 +83,37 @@ export function fingerprint(value: string): string {
72
83
  }
73
84
 
74
85
  /**
75
- * Pull the recorded cwd out of transcript text.
86
+ * Pull the recorded cwd out of transcript text: the FIRST row that records one.
76
87
  *
77
88
  * Claude puts it at the row's top level; Codex puts it in the session meta row's
78
89
  * `payload`. A torn trailing line is NORMAL — these files are appended to — so a
79
90
  * parse failure on any single row is skipped rather than fatal.
80
91
  *
81
- * Returns null when no row records one, and DISAGREEMENT is also null: a transcript
82
- * whose rows claim two different working directories is not something to guess at.
92
+ * THE FIRST cwd IS THE WORKSPACE (6.48.2). Until 6.48.1 two different values in the
93
+ * window refused the attach as a disagreement. On the release Mac every such
94
+ * transcript was one session whose shell `cd`-ed inside a turn (`operations/scripts`,
95
+ * a weekly folder): Claude stamps each row with the process cwd of the moment, all
96
+ * under the directory the session was started in. That starting directory is the
97
+ * workspace: it keys the transcript's project folder, it is the only place
98
+ * `claude --resume` finds the session, and it is what the first row records.
99
+ *
100
+ * Returns null only when no row in the window records one.
83
101
  */
84
102
  export function cwdFromTranscript(text: string): string | null {
85
- const seen = new Set<string>()
103
+ return scanForCwd(text).cwd
104
+ }
105
+
106
+ /**
107
+ * The scan with its outcome: the cwd, or whether the window ended INSIDE a row before
108
+ * any cwd was seen (a first prompt of pasted screenshots is one row of ~1 MB), which
109
+ * tells the resolver that a wider read can still answer.
110
+ */
111
+ export function scanForCwd(text: string): { cwd: string | null; endedMidRow: boolean; rows: number } {
86
112
  let rows = 0
87
- for (const line of text.split('\n')) {
113
+ const lines = text.split('\n')
114
+ // A window that does not end with a newline ended inside whatever row came last.
115
+ const endedMidRow = text.length > 0 && !text.endsWith('\n')
116
+ for (const line of lines) {
88
117
  if (rows >= WORKSPACE_SCAN_ROWS) break
89
118
  const trimmed = line.trim()
90
119
  if (!trimmed) continue
@@ -94,18 +123,14 @@ export function cwdFromTranscript(text: string): string | null {
94
123
  if (!row || typeof row !== 'object' || Array.isArray(row)) continue
95
124
  const record = row as Record<string, unknown>
96
125
  const direct = record.cwd
97
- if (typeof direct === 'string' && direct.length > 0) seen.add(direct)
126
+ if (typeof direct === 'string' && direct.length > 0) return { cwd: direct, endedMidRow: false, rows }
98
127
  const payload = record.payload
99
128
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
100
129
  const nested = (payload as Record<string, unknown>).cwd
101
- if (typeof nested === 'string' && nested.length > 0) seen.add(nested)
130
+ if (typeof nested === 'string' && nested.length > 0) return { cwd: nested, endedMidRow: false, rows }
102
131
  }
103
- // One consistent answer found early is enough; keep scanning only while it is
104
- // still the only one, so a disagreement is detected rather than short-circuited.
105
- if (seen.size > 1) return null
106
132
  }
107
- if (seen.size !== 1) return null
108
- return [...seen][0]!
133
+ return { cwd: null, endedMidRow, rows }
109
134
  }
110
135
 
111
136
  /**
@@ -145,17 +170,27 @@ export function resolveAttachedWorkspace(
145
170
  }
146
171
  if (provider !== 'claude' && provider !== 'codex') return null
147
172
  let path: string | null
148
- let text: string | null
173
+ let cwd: string | null = null
149
174
  try {
150
175
  path = deps.transcriptPath(provider, threadId)
151
176
  if (path === null) return null
152
- text = deps.readHead(path, WORKSPACE_SCAN_BYTES)
177
+ // Widen the head read while the window keeps ending inside a row with no cwd seen:
178
+ // 512 KiB, 2 MiB, 8 MiB, then the cap. A window that ended on a row boundary with
179
+ // no cwd is a transcript that records none, and widening would not change that.
180
+ let window = WORKSPACE_SCAN_BYTES
181
+ for (;;) {
182
+ const text = deps.readHead(path, window)
183
+ if (text === null) return null
184
+ const scan = scanForCwd(text)
185
+ cwd = scan.cwd
186
+ // The whole file came back (shorter than the window): there is nothing wider to read.
187
+ const wholeFile = Buffer.byteLength(text, 'utf8') < window
188
+ if (cwd !== null || wholeFile || !scan.endedMidRow || scan.rows >= WORKSPACE_SCAN_ROWS || window >= WORKSPACE_SCAN_BYTES_MAX) break
189
+ window = Math.min(window * 4, WORKSPACE_SCAN_BYTES_MAX)
190
+ }
153
191
  } catch {
154
192
  return null
155
193
  }
156
- if (text === null) return null
157
-
158
- const cwd = cwdFromTranscript(text)
159
194
  if (cwd === null) return null
160
195
  // A relative cwd would resolve against the SERVER's working directory, which is
161
196
  // not a location we control and is never what the user meant.