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 46e260052..629395f4b 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,8 +903,24 @@ function logSnapshotEvent( } } +/** + * Decode one `readdirSync(..., { encoding: "buffer" })` entry to its UTF-8 + * name. + * + * 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.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/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 6a246c161..fedccc50c 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"; @@ -169,3 +171,56 @@ 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. + */ + 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([]); + }); + + /** + * 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")); + 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); + }); +}); 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 = "." }