From dd716647f8133cdfbbdc60c30031899d521b9d39 Mon Sep 17 00:00:00 2001 From: Commander Date: Sat, 10 Oct 2026 00:34:35 +0800 Subject: [PATCH 1/3] fix(snapshot): decode readdir buffer entries as UTF-8 so exclusions match readdirSync with encoding "buffer" returns a Uint8Array on Bun, not a Buffer, and Uint8Array.prototype.toString() yields comma-joined byte values ("46,100,...") rather than the name. Every comparison against BRAIN_SNAPSHOT_EXCLUDED_ENTRIES therefore missed, so each archive packed in .snapshots - its own directory - and every snapshot carried its predecessors and grew without bound, until a dream run passed its timeout and died mid-write. Decode either shape through one helper, and assert the archive carries neither excluded entry. --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 6 +++++ openclaw.plugin.json | 2 +- package.json | 2 +- plugin.yaml | 2 +- plugins/codex/.codex-plugin/plugin.json | 2 +- plugins/hermes/plugin.yaml | 2 +- pyproject.toml | 2 +- src/core/brain/snapshot.ts | 16 +++++++++++- tests/core/brain/snapshot-members.test.ts | 32 +++++++++++++++++++++++ uv.lock | 2 +- 12 files changed, 62 insertions(+), 10 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a68be85c5..0f677a827 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.77.0", + "version": "1.77.1", "description": "Plugin-first second brain package for AI agents and humans.", "author": { "name": "Open Second Brain contributors" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 4610d18f7..451aef949 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.77.0", + "version": "1.77.1", "description": "Plugin-first second brain package for Codex, Hermes, Claude Code, OpenClaw, and other agent runtimes.", "author": { "name": "Open Second Brain contributors", diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d89b11e..0f88b02bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.77.1] - 2026-10-10 + +### Fixed + +- **The snapshot archive no longer carries its own home.** `createSnapshot` lists the top-level `Brain/` entries with `readdirSync(dir, { encoding: "buffer" })` and drops the ones `BRAIN_SNAPSHOT_EXCLUDED_ENTRIES` names, so `.snapshots` and `.artifacts` are never archived. On Bun that call returns a `Uint8Array`, not a `Buffer`, and `Uint8Array.prototype.toString()` renders comma-joined byte values (`"46,100,..."`) rather than the name, so every comparison missed and the exclusion never fired. Both entries were therefore packed into each archive: `.artifacts` churn, and `.snapshots` the archive home itself, which made every snapshot carry its predecessors and grow without bound. Dream snapshots doubled daily from single-digit megabytes to gigabytes until the run exceeded its timeout and died mid-write, leaving partial archives behind. One `bufferEntryName` helper decodes either shape and the filter uses it; the members are unchanged otherwise. A test asserts the archive carries neither excluded entry. + ## [1.77.0] - 2026-10-08 Open Second Brain now keeps its claim ledger in time and correctable: a claim can carry a validity window frozen from its source at ingest, the events recall reads the ledger over an assertion-time slice, an agent's stated claims commit only when they anchor in their assertion text, and two same-slot claims with disjoint windows stand in a succession channel instead of a conflict. One correct verb discovers a correction's blast radius and retires what it touches, and every serving surface serves a retired-but-serveable record only beside its resolved, readable chain-tip correction, fail-closed. The deep relational recall arm runs under width budgets with hub skipping, walks entity co-occurrence bridges, reports per-node path provenance gated at the caller's reach, and never lets a traversal-only row outrank what the lanes matched. diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 932f28d12..a105ad115 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "open-second-brain", "name": "Open Second Brain", "description": "Second brain for AI agents using Obsidian-compatible Markdown vaults.", - "version": "1.77.0", + "version": "1.77.1", "activation": { "onStartup": true }, "skills": ["./skills"], "contracts": { diff --git a/package.json b/package.json index 5c18c8039..69b87df1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.77.0", + "version": "1.77.1", "private": false, "description": "Second brain for AI agents using Obsidian-compatible Markdown vaults. Works with Hermes, Claude Code, Codex, and OpenClaw.", "keywords": [ diff --git a/plugin.yaml b/plugin.yaml index 76e53d686..a05614102 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: open-second-brain -version: "1.77.0" +version: "1.77.1" description: "Open Second Brain - native Hermes memory provider backed by an Obsidian-compatible Markdown vault." author: "Open Second Brain contributors" memory_provider: true diff --git a/plugins/codex/.codex-plugin/plugin.json b/plugins/codex/.codex-plugin/plugin.json index 6c36d364f..ab625d47c 100644 --- a/plugins/codex/.codex-plugin/plugin.json +++ b/plugins/codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.77.0", + "version": "1.77.1", "description": "Plugin-first second brain package for Codex, Hermes, Claude Code, OpenClaw, and other agent runtimes.", "author": { "name": "Open Second Brain contributors", diff --git a/plugins/hermes/plugin.yaml b/plugins/hermes/plugin.yaml index 76e53d686..a05614102 100644 --- a/plugins/hermes/plugin.yaml +++ b/plugins/hermes/plugin.yaml @@ -1,5 +1,5 @@ name: open-second-brain -version: "1.77.0" +version: "1.77.1" description: "Open Second Brain - native Hermes memory provider backed by an Obsidian-compatible Markdown vault." author: "Open Second Brain contributors" memory_provider: true diff --git a/pyproject.toml b/pyproject.toml index e01288b01..c49905865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" # CLI entry points (those moved to `package.json` `bin`). [project] name = "open-second-brain" -version = "1.77.0" +version = "1.77.1" description = "Hermes Python shim for Open Second Brain. Most of the project (CLI, MCP server, OpenClaw plugin) is TypeScript on Bun; see package.json." readme = "README.md" requires-python = ">=3.11" diff --git a/src/core/brain/snapshot.ts b/src/core/brain/snapshot.ts index 46e260052..dfe2a1b7c 100644 --- a/src/core/brain/snapshot.ts +++ b/src/core/brain/snapshot.ts @@ -776,7 +776,7 @@ export function createSnapshot( let topEntries: Buffer[]; try { topEntries = readdirSync(dirs.brain, { encoding: "buffer" }).filter( - (e) => !isSnapshotExcludedEntry(e.toString()), + (e) => !isSnapshotExcludedEntry(bufferEntryName(e)), ); } catch (err) { discardStoreArchive(derivedStore, vault, runId); @@ -903,6 +903,20 @@ function logSnapshotEvent( } } +/** + * Decode one `readdirSync(..., { encoding: "buffer" })` entry to its UTF-8 + * name. + * + * Bun returns a `Uint8Array` here, not a `Buffer`, and `Uint8Array.toString()` + * yields comma-joined byte values (`"46,100,..."`) instead of text. Every + * exclusion comparison therefore missed, and `.snapshots` - this archive's + * own directory - got packed into every archive, which is the exponential + * growth. `Buffer.from` decodes either shape. + */ +function bufferEntryName(e: Uint8Array | Buffer): string { + return Buffer.isBuffer(e) ? e.toString("utf8") : Buffer.from(e).toString("utf8"); +} + /** True for a top-level `Brain/` entry the snapshot family never touches. */ function isSnapshotExcludedEntry(name: string): boolean { return BRAIN_SNAPSHOT_EXCLUDED_ENTRIES.includes(name); diff --git a/tests/core/brain/snapshot-members.test.ts b/tests/core/brain/snapshot-members.test.ts index 6a246c161..20bd89d3b 100644 --- a/tests/core/brain/snapshot-members.test.ts +++ b/tests/core/brain/snapshot-members.test.ts @@ -169,3 +169,35 @@ describe("sortedMembers", () => { }, ); }); + +describe("the entries the snapshot family never touches", () => { + /** + * `.snapshots` and `.artifacts` are documented as never archived: the first + * because archiving the archive home makes every snapshot carry its + * predecessors and grow without bound, the second because it is ephemeral + * cache nobody wants restored. The exclusion is a string comparison against + * `readdirSync`'s buffer entries - which Bun returns as `Uint8Array`, whose + * `.toString()` is comma-joined byte values, not the name. Decoded that way + * nothing ever matched and the archive carried its own directory. + */ + test("the archive carries neither .snapshots nor .artifacts", () => { + const dirs = brainDirs(vault); + const snapshots = join(dirs.brain, ".snapshots"); + const artifacts = join(dirs.brain, ".artifacts"); + mkdirSync(snapshots, { recursive: true }); + writeFileSync(join(snapshots, "dream-prior.tar.zst"), "a prior archive"); + mkdirSync(artifacts, { recursive: true }); + writeFileSync(join(artifacts, "cache.json"), "{}"); + writeFileSync(join(dirs.brain, "kept.md"), "kept"); + + const res = createSnapshot(vault, "dream-excluded", { reason: DREAM }); + const members = listSnapshotArchive(res.path) + .split(/\r?\n/) + .map((l) => l.replace(/\/$/, "")) + .filter(Boolean); + + expect(members).toContain("Brain/kept.md"); + expect(members.filter((m) => m.includes(".snapshots"))).toEqual([]); + expect(members.filter((m) => m.includes(".artifacts"))).toEqual([]); + }); +}); diff --git a/uv.lock b/uv.lock index 3c365a123..0cb012251 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.11" [[package]] name = "open-second-brain" -version = "1.77.0" +version = "1.77.1" source = { editable = "." } From 1be27e3b0e95cdd1ba126d5ef15d59aab150ea90 Mon Sep 17 00:00:00 2001 From: Commander Date: Sat, 10 Oct 2026 18:39:27 +0800 Subject: [PATCH 2/3] test(snapshot): pin the Uint8Array decode arm directly The archive test only reaches the `Buffer.isBuffer(e)` arm: Bun's `readdirSync(..., { encoding: "buffer" })` hands back `Buffer` instances, so reverting `bufferEntryName` to `e.toString()` would leave it green and the `Uint8Array` arm - the one that exists precisely because Bun yields byte values instead of text - untested. Feed a bare `Uint8Array` straight to the decoders, single-byte and multibyte UTF-8, so the arm is pinned rather than assumed. Reverting the fix now turns both red. Also adds the missing `[1.77.1]` comparison reference. --- CHANGELOG.md | 1 + src/core/brain/snapshot.ts | 4 ++-- tests/core/brain/snapshot-members.test.ts | 29 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f88b02bb..4072c3aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8347,6 +8347,7 @@ plugin config (vault field)`, and exits with a clear - Sandbox vault and plugin manifest fixtures for tests. - GitHub release workflow for tag-based and manually dispatched releases. +[1.77.1]: https://github.com/itechmeat/open-second-brain/compare/v1.77.0...v1.77.1 [1.77.0]: https://github.com/itechmeat/open-second-brain/compare/v1.76.0...v1.77.0 [1.76.0]: https://github.com/itechmeat/open-second-brain/compare/v1.75.0...v1.76.0 [1.75.0]: https://github.com/itechmeat/open-second-brain/compare/v1.74.1...v1.75.0 diff --git a/src/core/brain/snapshot.ts b/src/core/brain/snapshot.ts index dfe2a1b7c..bb217547a 100644 --- a/src/core/brain/snapshot.ts +++ b/src/core/brain/snapshot.ts @@ -913,12 +913,12 @@ function logSnapshotEvent( * own directory - got packed into every archive, which is the exponential * growth. `Buffer.from` decodes either shape. */ -function bufferEntryName(e: Uint8Array | Buffer): string { +export function bufferEntryName(e: Uint8Array | Buffer): string { return Buffer.isBuffer(e) ? e.toString("utf8") : Buffer.from(e).toString("utf8"); } /** True for a top-level `Brain/` entry the snapshot family never touches. */ -function isSnapshotExcludedEntry(name: string): boolean { +export function isSnapshotExcludedEntry(name: string): boolean { return BRAIN_SNAPSHOT_EXCLUDED_ENTRIES.includes(name); } diff --git a/tests/core/brain/snapshot-members.test.ts b/tests/core/brain/snapshot-members.test.ts index 20bd89d3b..25a9c0671 100644 --- a/tests/core/brain/snapshot-members.test.ts +++ b/tests/core/brain/snapshot-members.test.ts @@ -33,7 +33,9 @@ import { bootstrapBrain } from "../../../src/core/brain/init.ts"; import { brainDirs } from "../../../src/core/brain/paths.ts"; import { BrainSnapshotError, + bufferEntryName, createSnapshot, + isSnapshotExcludedEntry, sortedMembers, } from "../../../src/core/brain/snapshot.ts"; import { system32Tool } from "../../../src/core/brain/secrets/owner-acl.ts"; @@ -200,4 +202,31 @@ describe("the entries the snapshot family never touches", () => { expect(members.filter((m) => m.includes(".snapshots"))).toEqual([]); expect(members.filter((m) => m.includes(".artifacts"))).toEqual([]); }); + + /** + * Bun's `readdirSync(..., { encoding: "buffer" })` hands back `Buffer` + * instances, so the archive test above only ever reaches the + * `Buffer.isBuffer(e)` arm. The `Uint8Array` arm - the one that actually + * exists to decode byte values instead of comma-joined numbers - would + * stay untested, and reverting the fix to `e.toString()` would leave that + * test green. These two feed a bare `Uint8Array` straight to the decoders + * so the arm is pinned rather than merely assumed. + */ + test("a bare Uint8Array entry decodes to its name, not to byte values", () => { + const excluded = new Uint8Array(new TextEncoder().encode(".snapshots")); + expect(Buffer.isBuffer(excluded)).toBe(false); + expect(bufferEntryName(excluded)).toBe(".snapshots"); + expect(isSnapshotExcludedEntry(bufferEntryName(excluded))).toBe(true); + + const kept = new Uint8Array(new TextEncoder().encode("kept.md")); + expect(bufferEntryName(kept)).toBe("kept.md"); + expect(isSnapshotExcludedEntry(bufferEntryName(kept))).toBe(false); + }); + + test("a bare Uint8Array holding multibyte UTF-8 decodes whole", () => { + const name = "笔记-2026.md"; + const arr = new Uint8Array(new TextEncoder().encode(name)); + expect(bufferEntryName(arr)).toBe(name); + expect(isSnapshotExcludedEntry(bufferEntryName(arr))).toBe(false); + }); }); From a237043c45622892f9200ac1a17a3369d8faa027 Mon Sep 17 00:00:00 2001 From: Sergey Eroshenkov Date: Mon, 12 Oct 2026 14:03:04 +0200 Subject: [PATCH 3/3] fix(snapshot): simplify the entry decoder, pin the explorer test port, release 1.81.1 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- CHANGELOG.md | 12 ++++++++++++ openclaw.plugin.json | 2 +- package.json | 2 +- plugin.yaml | 2 +- plugins/codex/.codex-plugin/plugin.json | 2 +- plugins/hermes/plugin.yaml | 2 +- pyproject.toml | 2 +- src/core/brain/snapshot.ts | 14 ++++++++------ tests/cli/brain-explorer.test.ts | 9 +++++---- tests/core/brain/snapshot-members.test.ts | 16 +++++----------- uv.lock | 2 +- 13 files changed, 39 insertions(+), 30 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5af94f15b..954cc0e4d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.81.0", + "version": "1.81.1", "description": "Plugin-first second brain package for AI agents and humans.", "author": { "name": "Open Second Brain contributors" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 6a9ec0c3a..21db4eccf 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.81.0", + "version": "1.81.1", "description": "Plugin-first second brain package for Codex, Hermes, Claude Code, OpenClaw, and other agent runtimes.", "author": { "name": "Open Second Brain contributors", diff --git a/CHANGELOG.md b/CHANGELOG.md index a6226e41a..5d69359ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.81.1] - 2026-10-12 + +### Fixed + +- **The snapshot exclusion no longer depends on the shape of a directory entry.** `createSnapshot` lists the top-level `Brain/` entries with `readdirSync(dir, { encoding: "buffer" })` and drops `.snapshots` and `.artifacts` by name. The name was read with `toString()`, which is right for a `Buffer` but renders a bare `Uint8Array` as comma-joined byte values (`"46,100,..."`); on a runtime that returns that shape nothing matched, `.snapshots` was packed into every archive, and each dream snapshot carried its predecessors until a run outgrew its timeout. Entries now decode through `Buffer.from`, which reads either shape, and tests pin both the archive members and the bare `Uint8Array` decode. Bun 1.4.0 and 1.4.2 on Linux return `Buffer` entries, so vaults there were not affected. Reported and fixed by @Iversondwx in #243. +- **The explorer's port-in-use test no longer collides on busy runners.** It held a random port and failed when another process already owned it; it now holds a port the OS assigns. + +### Changed + +- **Dependencies:** `oxfmt` 0.72.0, `oxlint` 1.87.0, `@types/node` 22.20.5 and `@node-rs/jieba` 2.0.4; CI actions `actions/upload-artifact` 7.0.2 and `hashgraph-online/ai-plugin-scanner-action` 1.2.772. + ## [1.81.0] - 2026-10-11 Open Second Brain 1.81.0 is the answer-trust release: it narrows the gap between what an agent was given and what its answer can be believed on. A Stop-hook step now checks the quotations in the assistant's own answer against the note cited for them or the evidence the injection ledger actually delivered, a stated claim can name the context-pack sample it drew on and read the receipt and outcome provenance back, trusted local-file ingest keeps the source's bytes in a durable content-addressed archive instead of trusting a file to stay where it was, and a record with no session is read as no-session everywhere instead of pooling into one phantom bucket. A repair lane closes the outstanding review findings: attribute narrowing in search no longer silently excludes documents whose indexed snapshot has gone stale, and the wave's own review hardened the annotation channel, the provenance reach and the archive's write path. @@ -8449,6 +8460,7 @@ plugin config (vault field)`, and exits with a clear - Sandbox vault and plugin manifest fixtures for tests. - GitHub release workflow for tag-based and manually dispatched releases. +[1.81.1]: https://github.com/itechmeat/open-second-brain/compare/v1.81.0...v1.81.1 [1.81.0]: https://github.com/itechmeat/open-second-brain/compare/v1.80.0...v1.81.0 [1.80.0]: https://github.com/itechmeat/open-second-brain/compare/v1.79.0...v1.80.0 [1.79.0]: https://github.com/itechmeat/open-second-brain/compare/v1.78.0...v1.79.0 diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 24f806134..41161ddeb 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "open-second-brain", "name": "Open Second Brain", "description": "Second brain for AI agents using Obsidian-compatible Markdown vaults.", - "version": "1.81.0", + "version": "1.81.1", "activation": { "onStartup": true }, "skills": ["./skills"], "contracts": { diff --git a/package.json b/package.json index 8cc77f71e..faf5723b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.81.0", + "version": "1.81.1", "private": false, "description": "Second brain for AI agents using Obsidian-compatible Markdown vaults. Works with Hermes, Claude Code, Codex, and OpenClaw.", "keywords": [ diff --git a/plugin.yaml b/plugin.yaml index 679885b61..25ee9ae9d 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,5 +1,5 @@ name: open-second-brain -version: "1.81.0" +version: "1.81.1" description: "Open Second Brain - native Hermes memory provider backed by an Obsidian-compatible Markdown vault." author: "Open Second Brain contributors" memory_provider: true diff --git a/plugins/codex/.codex-plugin/plugin.json b/plugins/codex/.codex-plugin/plugin.json index da528a103..c56952fe0 100644 --- a/plugins/codex/.codex-plugin/plugin.json +++ b/plugins/codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "open-second-brain", - "version": "1.81.0", + "version": "1.81.1", "description": "Plugin-first second brain package for Codex, Hermes, Claude Code, OpenClaw, and other agent runtimes.", "author": { "name": "Open Second Brain contributors", diff --git a/plugins/hermes/plugin.yaml b/plugins/hermes/plugin.yaml index 679885b61..25ee9ae9d 100644 --- a/plugins/hermes/plugin.yaml +++ b/plugins/hermes/plugin.yaml @@ -1,5 +1,5 @@ name: open-second-brain -version: "1.81.0" +version: "1.81.1" description: "Open Second Brain - native Hermes memory provider backed by an Obsidian-compatible Markdown vault." author: "Open Second Brain contributors" memory_provider: true diff --git a/pyproject.toml b/pyproject.toml index b8578f8c4..1cc44186d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" # CLI entry points (those moved to `package.json` `bin`). [project] name = "open-second-brain" -version = "1.81.0" +version = "1.81.1" description = "Hermes Python shim for Open Second Brain. Most of the project (CLI, MCP server, OpenClaw plugin) is TypeScript on Bun; see package.json." readme = "README.md" requires-python = ">=3.11" diff --git a/src/core/brain/snapshot.ts b/src/core/brain/snapshot.ts index bb217547a..629395f4b 100644 --- a/src/core/brain/snapshot.ts +++ b/src/core/brain/snapshot.ts @@ -907,14 +907,16 @@ function logSnapshotEvent( * Decode one `readdirSync(..., { encoding: "buffer" })` entry to its UTF-8 * name. * - * Bun returns a `Uint8Array` here, not a `Buffer`, and `Uint8Array.toString()` - * yields comma-joined byte values (`"46,100,..."`) instead of text. Every - * exclusion comparison therefore missed, and `.snapshots` - this archive's - * own directory - got packed into every archive, which is the exponential - * growth. `Buffer.from` decodes either shape. + * The exclusion below is a string comparison, so it holds only while the + * entry decodes to its name. Bun and Node document `Buffer` entries, whose + * `toString()` is the name, but a runtime that hands back a bare + * `Uint8Array` renders comma-joined byte values (`"46,100,..."`) instead: + * nothing matches, and `.snapshots` - the archive home - is packed into + * every archive, so each one carries its predecessors. `Buffer.from` + * decodes either shape. */ export function bufferEntryName(e: Uint8Array | Buffer): string { - return Buffer.isBuffer(e) ? e.toString("utf8") : Buffer.from(e).toString("utf8"); + return Buffer.from(e).toString("utf8"); } /** True for a top-level `Brain/` entry the snapshot family never touches. */ diff --git a/tests/cli/brain-explorer.test.ts b/tests/cli/brain-explorer.test.ts index 613ca1103..b11663925 100644 --- a/tests/cli/brain-explorer.test.ts +++ b/tests/cli/brain-explorer.test.ts @@ -282,14 +282,15 @@ describe("o2b brain explorer (live)", () => { test("port in use exits 1 with a friendly message", async () => { await bootstrap(); - const port = pickPort(); - // Hold the port from inside the test process so the CLI hits - // EADDRINUSE. + // Hold a port the OS picked from inside the test process so the CLI + // hits EADDRINUSE; a random pick can collide with a port another + // process already holds, and then the blocker itself fails to bind. const blocker = Bun.serve({ hostname: "127.0.0.1", - port, + port: 0, fetch: () => new Response("blocker"), }); + const port = blocker.port; try { const r = await runCli(["brain", "explorer", "--vault", vault, "--port", String(port)], { env: { OPEN_SECOND_BRAIN_CONFIG: config }, diff --git a/tests/core/brain/snapshot-members.test.ts b/tests/core/brain/snapshot-members.test.ts index 25a9c0671..fedccc50c 100644 --- a/tests/core/brain/snapshot-members.test.ts +++ b/tests/core/brain/snapshot-members.test.ts @@ -177,10 +177,7 @@ describe("the entries the snapshot family never touches", () => { * `.snapshots` and `.artifacts` are documented as never archived: the first * because archiving the archive home makes every snapshot carry its * predecessors and grow without bound, the second because it is ephemeral - * cache nobody wants restored. The exclusion is a string comparison against - * `readdirSync`'s buffer entries - which Bun returns as `Uint8Array`, whose - * `.toString()` is comma-joined byte values, not the name. Decoded that way - * nothing ever matched and the archive carried its own directory. + * cache nobody wants restored. */ test("the archive carries neither .snapshots nor .artifacts", () => { const dirs = brainDirs(vault); @@ -204,13 +201,10 @@ describe("the entries the snapshot family never touches", () => { }); /** - * Bun's `readdirSync(..., { encoding: "buffer" })` hands back `Buffer` - * instances, so the archive test above only ever reaches the - * `Buffer.isBuffer(e)` arm. The `Uint8Array` arm - the one that actually - * exists to decode byte values instead of comma-joined numbers - would - * stay untested, and reverting the fix to `e.toString()` would leave that - * test green. These two feed a bare `Uint8Array` straight to the decoders - * so the arm is pinned rather than merely assumed. + * Bun and Node hand back `Buffer` entries, so the archive test above + * never sees a bare `Uint8Array`, and reverting the decoder to + * `e.toString()` would leave it green. These two feed a bare `Uint8Array` + * straight to the decoders so that shape is pinned rather than assumed. */ test("a bare Uint8Array entry decodes to its name, not to byte values", () => { const excluded = new Uint8Array(new TextEncoder().encode(".snapshots")); diff --git a/uv.lock b/uv.lock index 574cfbc16..17ecae5a0 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.11" [[package]] name = "open-second-brain" -version = "1.81.0" +version = "1.81.1" source = { editable = "." }