@gotcos/glasses-server 6.22.0 → 6.22.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,25 @@
1
+ ## 6.22.1
2
+
3
+ Notes attached from somewhere else by a symlink are now read properly. Found by
4
+ Queen within hours of 6.22.0, on the very first real setup.
5
+
6
+ - **A symlinked subfolder or file was silently skipped.** `readdirSync` reports a
7
+ symlink as `isSymbolicLink()`, never as `isDirectory()` or `isFile()`, so the walk
8
+ ignored every link it met. A top-level `memory -> /elsewhere` link worked only by
9
+ accident, because the folder LOOKUP uses `statSync` and follows links while the
10
+ walk did not. Anything linked one level deeper vanished with no error at all.
11
+ - Attaching an existing store is a primary way to adopt this feature, not an edge
12
+ case, so a link now behaves like whatever it points at: a linked folder is walked,
13
+ a linked note is read, and a broken link is skipped without failing the read.
14
+ - **Cycles terminate.** Following links makes `memory/loop -> memory` fatal, so every
15
+ directory is now visited once by resolved real path. The same identity check stops
16
+ a store reached through two different links being counted twice.
17
+ - **No depth limit.** An earlier draft of this fix capped nesting at 16 levels; that
18
+ guarded nothing real (cycles terminate on identity, and the file cap already bounds
19
+ the work) while silently hiding notes nested deeper. Removed.
20
+ - The status counter and the reader walk the same way, so the header count and the
21
+ list can no longer disagree.
22
+
1
23
  ## 6.22.0
2
24
 
3
25
  Memory and Threads now work without a Python bridge, a venv, or a vector
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.22.0",
3
+ "version": "6.22.1",
4
4
  "description": "COS Glasses — 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": {
@@ -19,7 +19,7 @@
19
19
  // vector store. The files were always the substrate — which is exactly why an
20
20
  // amendment must write only here and never to the derived store.
21
21
 
22
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
22
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
23
23
  import { homedir } from 'node:os'
24
24
  import { basename, dirname, extname, join, resolve } from 'node:path'
25
25
 
@@ -85,26 +85,72 @@ function findDir(root: string, names: readonly string[]): string | null {
85
85
  return null
86
86
  }
87
87
 
88
+ /** Extensions the readers treat as notes. */
89
+ const NOTE_EXTENSIONS = ['.md', '.markdown', '.txt']
90
+
88
91
  /**
89
- * Every markdown file under `dir`, recursively, bounded.
92
+ * Every markdown file under `dir`, recursively, bounded, FOLLOWING SYMLINKS.
90
93
  *
91
94
  * Any nesting is accepted on purpose — a user's notes may be organised by year, by
92
95
  * project, or not at all, and rejecting a shape is what made COS Data unusable for
93
96
  * anyone but its author.
97
+ *
98
+ * SYMLINKS ARE FIRST-CLASS, and that is a correction. A `Dirent` reports a symlink
99
+ * as `isSymbolicLink()`, NOT as `isDirectory()` or `isFile()`, so the first version
100
+ * of this walk silently skipped every linked file and linked subfolder. Queen hit
101
+ * exactly that on 2026-08-09: she wired `operations/memory` at her real note store
102
+ * with a symlink, which worked only because `findDir` uses `statSync` and follows
103
+ * links — anything linked one level deeper would have vanished with no error.
104
+ *
105
+ * Attaching notes that live somewhere else is a PRIMARY use case, not an edge case,
106
+ * so a link has to behave like the thing it points at.
107
+ *
108
+ * Cycles are the cost of following them: `a -> ../a` recurses forever. Every
109
+ * directory is recorded by its resolved real path and visited once, which also
110
+ * stops the same store being counted twice when two links reach it.
111
+ *
112
+ * There is deliberately NO depth limit. An earlier draft capped nesting at 16 as
113
+ * "bounding a pathological tree", but real-path identity already makes cycles
114
+ * terminate and MAX_FILES_SCANNED already bounds the work — so the cap's only real
115
+ * effect was to silently hide notes nested deeper than that. That is the same
116
+ * invisible-skip defect this function was just fixed for, moved further out, and a
117
+ * mutation removing the cap changed no test, which is how it got noticed.
94
118
  */
95
- function walkMarkdown(dir: string, budget = { left: MAX_FILES_SCANNED }): string[] {
119
+ function walkMarkdown(
120
+ dir: string,
121
+ budget = { left: MAX_FILES_SCANNED },
122
+ seen: Set<string> = new Set(),
123
+ ): string[] {
96
124
  const out: string[] = []
125
+ // Identity by real path, so a link and its target are the same directory.
126
+ let real: string
127
+ try { real = realpathSync(dir) } catch { return out }
128
+ if (seen.has(real)) return out
129
+ seen.add(real)
130
+
97
131
  let entries: import('node:fs').Dirent[]
98
132
  try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
99
133
  for (const entry of entries) {
100
134
  if (budget.left <= 0) break
101
135
  if (entry.name.startsWith('.')) continue
102
136
  const full = join(dir, entry.name)
137
+ const isNote = NOTE_EXTENSIONS.includes(extname(entry.name).toLowerCase())
103
138
  if (entry.isDirectory()) {
104
- out.push(...walkMarkdown(full, budget))
105
- } else if (entry.isFile() && ['.md', '.markdown', '.txt'].includes(extname(entry.name).toLowerCase())) {
139
+ out.push(...walkMarkdown(full, budget, seen))
140
+ } else if (entry.isFile()) {
141
+ if (!isNote) continue
106
142
  budget.left -= 1
107
143
  out.push(full)
144
+ } else if (entry.isSymbolicLink()) {
145
+ // Resolve to decide: a linked folder is walked, a linked note is read.
146
+ let target: import('node:fs').Stats
147
+ try { target = statSync(full) } catch { continue } // broken link, skip quietly
148
+ if (target.isDirectory()) {
149
+ out.push(...walkMarkdown(full, budget, seen))
150
+ } else if (target.isFile() && isNote) {
151
+ budget.left -= 1
152
+ out.push(full)
153
+ }
108
154
  }
109
155
  }
110
156
  return out