From 2e1146ea68519c0ad1c040f623f2faad8c2c433f Mon Sep 17 00:00:00 2001 From: Zuckerberg Date: Thu, 4 Jun 2026 10:32:17 -0700 Subject: [PATCH] Switch memory provider to hindsight --- common/sandboxed-workspace/base.nix | 1 + flake.lock | 17 ++++ flake.nix | 7 ++ machines/fry/workspaces/hermes.nix | 120 ++++++++++++++++++++++++++-- overlays/default.nix | 9 +++ pkgs/hindsight/default.nix | 72 +++++++++++++++++ 6 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 pkgs/hindsight/default.nix diff --git a/common/sandboxed-workspace/base.nix b/common/sandboxed-workspace/base.nix index 958fce2..b0f850d 100644 --- a/common/sandboxed-workspace/base.nix +++ b/common/sandboxed-workspace/base.nix @@ -31,6 +31,7 @@ in ]; nixpkgs.overlays = [ + hostConfig.inputs.self.overlays.default hostConfig.inputs.claude-code-nix.overlays.default ]; diff --git a/flake.lock b/flake.lock index 80180c8..24718a3 100644 --- a/flake.lock +++ b/flake.lock @@ -267,6 +267,22 @@ "type": "github" } }, + "hindsight-src": { + "flake": false, + "locked": { + "lastModified": 1780589667, + "narHash": "sha256-Zb8zIdlnm9spUiVbGdByGKE/V/5q+RKjbuSbayNIDxY=", + "owner": "vectorize-io", + "repo": "hindsight", + "rev": "7683f29004ab9d88e0ef3f0f18b8ffe6fc741905", + "type": "github" + }, + "original": { + "owner": "vectorize-io", + "repo": "hindsight", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -441,6 +457,7 @@ "flake-compat": "flake-compat", "flake-utils": "flake-utils", "hermes-agent": "hermes-agent", + "hindsight-src": "hindsight-src", "home-manager": "home-manager", "microvm": "microvm", "nix-index-database": "nix-index-database", diff --git a/flake.nix b/flake.nix index 9512935..816cfc8 100644 --- a/flake.nix +++ b/flake.nix @@ -94,6 +94,13 @@ uv2nix.inputs.pyproject-nix.follows = "hermes-agent/pyproject-nix"; }; }; + + # Hindsight memory server (vectorize-io). No upstream flake; consumed as a + # bare source tree and packaged via the uv2nix toolchain from hermes-agent. + hindsight-src = { + url = "github:vectorize-io/hindsight"; + flake = false; + }; }; outputs = { self, nixpkgs, ... }@inputs: diff --git a/machines/fry/workspaces/hermes.nix b/machines/fry/workspaces/hermes.nix index 72f1b1b..bc9e214 100644 --- a/machines/fry/workspaces/hermes.nix +++ b/machines/fry/workspaces/hermes.nix @@ -21,6 +21,9 @@ extraPackages = with pkgs; [ nix git ripgrep fd jq codex ]; + # Pulls in hindsight-client (the HTTP client lib the memory plugin uses). + extraDependencyGroups = [ "hindsight" ]; + environment = { SIGNAL_HTTP_URL = "http://127.0.0.1:8080"; CODEX_HOME = "/var/lib/hermes/.codex"; @@ -44,14 +47,14 @@ toolsets = [ "all" ]; terminal.backend = "local"; - # Self-hosted memory: pure SQLite in-process, no external services or - # API keys. db file lives under HERMES_HOME (= /var/lib/hermes/.hermes), - # which is on the persisted bind-mount. - memory.provider = "holographic"; - plugins.hermes-memory-store = { - db_path = "/var/lib/hermes/.hermes/memory_store.db"; - auto_extract = true; - default_trust = 0.5; + # Memory lives in a sibling hindsight-api process (see systemd unit + # below) backed by system postgres. Plugin talks HTTP to it locally. + memory.provider = "hindsight"; + plugins.hermes-hindsight = { + mode = "local_external"; + api_url = "http://127.0.0.1:8888"; + bank_id = "hermes"; + memory_mode = "hybrid"; }; }; }; @@ -80,6 +83,8 @@ systemd.tmpfiles.rules = [ "d /var/lib/hermes/.codex 0700 googlebot users -" "C+ /var/lib/hermes/.codex/config.toml 0644 googlebot users - /etc/hermes-codex-config.toml" + "d /var/lib/hermes/hindsight 0700 googlebot users -" + "d /var/lib/hermes/postgresql 0750 postgres postgres -" ]; # signal-cli runs an HTTP JSON-RPC daemon on localhost; hermes-agent talks to @@ -102,4 +107,103 @@ RestartSec = "5"; }; }; + + # --------------------------------------------------------------------------- + # Hindsight memory backend + # + # Architecture: + # hermes-agent --HTTP--> hindsight-api (port 8888) --asyncpg--> postgres + # hindsight-worker (background async retain/reflect) + # + # The api + worker run as googlebot with HOME=/var/lib/hermes so the + # `openai-codex` provider's hardcoded Path.home() / ".codex" / "auth.json" + # resolves to the persisted codex tokens. + # --------------------------------------------------------------------------- + + services.postgresql = { + enable = true; + package = pkgs.postgresql_17; + extensions = ps: [ ps.pgvector ]; + # Persist DB state on the /var/lib/hermes bind mount so the hindsight + # memory bank survives `nixos-rebuild switch` (which recreates the + # incus container and wipes its writable layer). + dataDir = "/var/lib/hermes/postgresql/17"; + ensureDatabases = [ "hindsight" ]; + ensureUsers = [{ + name = "googlebot"; + # Superuser so the hindsight-api alembic migrations can `CREATE EXTENSION` + # and own the schema. Acceptable here because postgres is local-only to + # the sandboxed workspace and only talks to hindsight-api over the unix + # socket. + ensureClauses.superuser = true; + }]; + }; + + systemd.services.hindsight-api = + let + env = { + # Connection — postgres is on the local unix socket; sqlalchemy gets + # an asyncpg URL via the asyncpg+host= trick. + HINDSIGHT_API_DATABASE_URL = "postgresql+asyncpg:///hindsight?host=/run/postgresql"; + HINDSIGHT_API_HOST = "127.0.0.1"; + HINDSIGHT_API_PORT = "8888"; + + # LLM + embeddings both ride codex OAuth — no separate keys. + HINDSIGHT_API_LLM_PROVIDER = "openai-codex"; + HINDSIGHT_API_EMBEDDINGS_PROVIDER = "openai-codex"; + + # We did not install [local-ml] extras, so disable the reranker + # (default is "local" which requires sentence-transformers). + HINDSIGHT_API_RERANKER_PROVIDER = "none"; + + # HOME drives where the codex auth file is read from + # (CodexLLM/CodexOAuthEmbeddings: Path.home() / ".codex" / "auth.json"). + HOME = "/var/lib/hermes"; + }; + in + { + description = "Hindsight memory API server"; + wantedBy = [ "multi-user.target" ]; + after = [ "postgresql.service" "network-online.target" ]; + wants = [ "network-online.target" ]; + requires = [ "postgresql.service" ]; + before = [ "hermes-agent.service" ]; + environment = env; + serviceConfig = { + Type = "simple"; + User = "googlebot"; + Group = "users"; + ExecStart = "${pkgs.hindsight-api}/bin/hindsight-api"; + Restart = "always"; + RestartSec = "5"; + WorkingDirectory = "/var/lib/hermes/hindsight"; + }; + }; + + systemd.services.hindsight-worker = + let + env = { + HINDSIGHT_API_DATABASE_URL = "postgresql+asyncpg:///hindsight?host=/run/postgresql"; + HINDSIGHT_API_LLM_PROVIDER = "openai-codex"; + HINDSIGHT_API_EMBEDDINGS_PROVIDER = "openai-codex"; + HINDSIGHT_API_RERANKER_PROVIDER = "none"; + HOME = "/var/lib/hermes"; + }; + in + { + description = "Hindsight background worker (async retain / reflect)"; + wantedBy = [ "multi-user.target" ]; + after = [ "hindsight-api.service" ]; + requires = [ "hindsight-api.service" ]; + environment = env; + serviceConfig = { + Type = "simple"; + User = "googlebot"; + Group = "users"; + ExecStart = "${pkgs.hindsight-api}/bin/hindsight-worker"; + Restart = "always"; + RestartSec = "5"; + WorkingDirectory = "/var/lib/hermes/hindsight"; + }; + }; } diff --git a/overlays/default.nix b/overlays/default.nix index 6d6ecaf..f15d882 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -52,4 +52,13 @@ in qtmultimedia qtwayland qtwebengine qcoro; inherit (prev) lib fetchFromGitLab pkg-config sdl3 libcec wayland; }; + + # Hindsight agent-memory server. Built via uv2nix against the upstream + # workspace; uses hermes-agent's toolchain pin to avoid duplicating uv2nix. + hindsight-api = prev.callPackage ../pkgs/hindsight { + hindsight-src = inputs.hindsight-src; + uv2nix = inputs.hermes-agent.inputs.uv2nix; + pyproject-nix = inputs.hermes-agent.inputs.pyproject-nix; + pyproject-build-systems = inputs.hermes-agent.inputs.pyproject-build-systems; + }; } diff --git a/pkgs/hindsight/default.nix b/pkgs/hindsight/default.nix new file mode 100644 index 0000000..fc74d34 --- /dev/null +++ b/pkgs/hindsight/default.nix @@ -0,0 +1,72 @@ +{ lib +, callPackage +, python312 +, stdenv +, makeWrapper + +, hindsight-src +, uv2nix +, pyproject-nix +, pyproject-build-systems +}: + +# Build the hindsight-api-slim venv from upstream's uv workspace. We pick +# `hindsight-api-slim` (not `hindsight-api`) so we skip the `[all]` extras +# (local-ml, embedded-db). With openai-codex provider for both LLM and +# embeddings, and system postgres as the store, those extras are unused weight. +let + workspace = uv2nix.lib.workspace.loadWorkspace { + workspaceRoot = hindsight-src; + }; + + overlay = workspace.mkPyprojectOverlay { + sourcePreference = "wheel"; + }; + + pythonSet = + (callPackage pyproject-nix.build.packages { + python = python312; + }).overrideScope + (lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + ]); + + venv = pythonSet.mkVirtualEnv "hindsight-api-env" { + hindsight-api-slim = [ ]; + }; +in +stdenv.mkDerivation { + pname = "hindsight-api"; + version = "0.7.2"; + + dontUnpack = true; + nativeBuildInputs = [ makeWrapper ]; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + # The venv ships hindsight-api / hindsight-worker / hindsight-admin / + # hindsight-local-mcp / alembic in $venv/bin. Link them out so callers + # don't have to know about the venv layout. + for bin in hindsight-api hindsight-worker hindsight-admin hindsight-local-mcp alembic; do + if [ -e ${venv}/bin/$bin ]; then + ln -s ${venv}/bin/$bin $out/bin/$bin + fi + done + runHook postInstall + ''; + + passthru = { + inherit venv pythonSet; + pythonEnv = venv; + }; + + meta = with lib; { + description = "Hindsight: agent memory with knowledge graph and tiered retrieval"; + homepage = "https://github.com/vectorize-io/hindsight"; + license = licenses.mit; + mainProgram = "hindsight-api"; + platforms = platforms.linux; + }; +}