Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion plugin.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/hermes/plugin.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 18 additions & 2 deletions src/core/brain/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

Expand Down
9 changes: 5 additions & 4 deletions tests/cli/brain-explorer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
55 changes: 55 additions & 0 deletions tests/core/brain/snapshot-members.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
});
});
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading