Skip to content

v1.72.0

Choose a tag to compare

@kitfunso kitfunso released this 11 Oct 11:32
· 335 commits to master since this release
3a32831

Added

  • A reader that says what happened to one lesson in one session. scripts/z10-reconstruct.mjs opens a store read-only and joins the memory, the delivery ledger rows, the host transcript and an optional application-label file. It returns the lesson's delivery class: not written, not retrieved, rejected, delivery unconfirmed, delivered with application unknown, applied but wrong, or applied with a supporting outcome. When the evidence cannot decide, it returns indeterminate with a reason. The script is a development tool and is not shipped in the package.
  • Labelled fixtures for every delivery class. 34 class reads and 12 negative controls drive the built CLI on scratch stores and check the reader against each case's raw ledger rows. Three more tests check that hippo context with a query and the three boundary hooks print, save and exit the same with the ledger off and on.
  • Tool failures in the delivery ledger. With deliveryLedger.enabled, hippo capture-error now writes one tool-failure row per failed tool call, linked to its failure log row. A second hook run for the same tool call is marked as a duplicate when the host sends a tool call id (Claude Code does; the Copilot CLI does not). The ledger stays off by default.
  • One store hash per store. Ledger rows written by this version (ledger_version 4) hash the store's resolved folder, so a store reached through a link or junction has one hash. Older rows still read.
  • HIPPO_SKIP_SCHEDULE=1 skips the daily run in hippo init, hippo init --scan and hippo setup, as --no-schedule does. It suits scripted and CI installs that should never touch the machine's crontab or Task Scheduler.

Changed

  • Schema v55. Adds two columns and one index to delivery_events. Nothing is backfilled, and an older binary can still open and write the store.
  • Internal: the store-level createApiKey, which no published entry exports, now mints a member key that expires after 90 days when the caller names no role or expiry, as authCreate already did. A caller that needs an admin key passes role: 'admin', and one that needs another lifetime passes expiresAt; it can no longer mint a key that never expires.
  • Internal: the schema-fit check on every local remember reads only the tenant's newest 2000 rows instead of every row twice.
  • Internal: invalidation writes its weakened rows in one transaction instead of one per row.
  • Internal: project repair and the refine command fetch rows by id in one query instead of scanning all rows or reading one parent at a time.
  • hippo recall with a global store reads each store once. The ranking reuses the candidates it already loaded instead of loading both stores a second time. Results are unchanged.
  • The ambient context backfill stops reading once it has enough rows. When the newest rows are refused, it reads older rows in growing pages until it keeps the number it needs, instead of reading every remaining row. The rows it injects are unchanged.
  • The legacy store import keeps the newest 50 sleep runs, the same cap every hippo sleep already applies, so stats.json matches the table from the first open.
  • Internal: goal policies load in one query, the stats rewrite on every remember and recall reads at most 50 sleep runs, and a recall's audit rows, trace and retrieval updates commit in one transaction.
  • Internal: sleep reads the whole store once after consolidation and shares that read, with the set of rows backing an object, between dedup, the quality audit and the ambient summary. A sleep now makes 2 whole-store reads, down from 3.
  • Internal: invalidateMatching with onlyId reads that one row, scoped to the tenant, instead of every row the tenant holds.
  • Internal: DAG fact clustering compares a fact only with facts that share an entity tag, through the overlap index, and returns the same clusters as the all-pairs scan.
  • Internal: syncing the global store down reads id, tenant, text and source to decide what to copy, reads only those rows whole, and commits every copy in one transaction on one handle per store. A rejected value still skips only its own row and keeps its refusal audit row.
  • The autolearn git check runs git directly instead of through a shell. It saves one process start per repo checked, which on Windows is the slow part of hippo init --scan.
  • Sleep's physics pass moves at most 1000 particles. Each substep compares every particle with every other, so a large store made one sleep take minutes. Past the cap, sleep moves the 1000 most recently recalled memories and leaves the rest as stored; the details line says how many it picked. Stores under the cap get the same results as before.
  • An MCP remember that starts an auto-sleep inside hippo serve runs the consolidation in a child process. It shares the queue and deadline of POST /v1/sleep, so the server keeps answering while it runs. Stdio MCP still consolidates in its own process.
  • Internal: hippo embed status and hippo dedup count by id instead of decoding every vector or loading every row.
  • Internal: hippo dag looks up each summary's children in one index instead of filtering the store once per summary.
  • Internal: the MCP stdio loop joins pending input only when a frame can have ended, so a large frame split over many chunks is no longer re-copied per chunk.
  • Internal: hippo learn and auto-share write their rows in one transaction instead of one per row; auto-share reuses the rows it already read.
  • Pattern invalidation reads only rows that could match. hippo invalidate prefilters rows in SQL on the pattern's words and tag, then run the same exact match, so a large store no longer decodes every row to weaken a few.
  • Internal: the sleep-cycle summary rebuild reads one row past its cap instead of every dirty summary, and the maintenance store count is a SQL COUNT(*) instead of a full id list.
  • hippo resolve and the MCP hippo_resolve tool now refuse a conflict whose memories are no longer live (superseded, archived or quarantined) and a --keep id outside the pair, as the dashboard already did.
  • hippo resolve now applies the same personal-row check as MCP and the dashboard, so an unowned admin cannot resolve a conflict holding someone's personal memory.
  • hippo resolve reports a refused conflict as Error: <reason> with exit code 1, naming why it was refused.
  • Internal: resolveMemoryConflict in the api layer owns conflict resolution for MCP and the CLI; the dashboard moves to it in a follow-up.
  • Internal: reject by id, authKeyTenant and the MCP share tool read through the store port, so a served store answers them.
  • Internal: The short token fingerprint, the cosine score, the stdout and stderr log tee and the recall read rules each have one copy. A log file that stops accepting writes now warns once instead of dropping output silently. Recall availability uses the shared hour constant.
  • hippo recall --help, explain --help, eval --help and auth create now print the embedding weight, MMR lambda, graph hops, graph seeds and key lifetime defaults the code uses, instead of copies typed into the help.
  • Internal: ambient summary cut points and replay weights are named constants; continuityTokensOf no longer shadows tokenize; loadPredictionList has a return type; comment and string lines over 160 characters are wrapped.
  • Internal: capture code no longer opens the store or names a connection type. The write gate (gated-write) and the compaction record's SQL moved into src/store; the spool, transcript reads and hook text stay in src/capture, which calls store functions that take a folder and plain values.
  • Internal: the agent-home path helpers live in src/util/agent-homes.ts and mergedSuccessor in src/util/merged-row.ts, which cuts the hooks/agent-memories and trust/consolidate import cycles; the store-port ratchet now also counts files outside the data layer that name a raw database handle type.
  • A failed Slack signature check on hippo slack dlq replay now counts as a retry, like GitHub. Both connectors now bump retry_count by one and stamp retried_at on every failed replay; Slack used to leave retried_at empty and skip the bump on sig_fail.
  • Internal: the failed-replay bump lives once in src/connectors/dlq.ts (failAndBump), and each connector supplies only its table's bump.
  • Internal: the synchronous recall() in src/api/recall.ts is gone; retrieve() is the one recall entry, and its tests, plus two benchmarks, now call it.
  • Internal: The MCP admin tools (status, conflicts, share, base rate) read through the api layer instead of calling store modules directly.
  • Internal: hippo projects and the once-per-sleep project tag repair reach hippo.db through new api functions (listProjectNames, mergeProjectNames, repairProjectNames, repairProjectNamesOnce). The store opens the handle and owns the backup, transaction and mirror refresh; the merge and repair rules no longer hold a raw database handle. No CLI opens hippo.db itself any more.
  • Internal: src/store now holds only code that runs SQL on hippo.db, plus its row shapes, SQL text and adapter. Scope predicates moved to src/core/recall-scope.ts (their SQL twins scopeAdmitSql and touchableScopeSql joined src/store/rule-sql.ts), the markdown mirror format to src/core/markdown.ts, typed-object shapes to src/core/object-types.ts, RejectedValueError to src/core/api-errors.ts, the YAML frontmatter codec to src/util/yaml.ts, hook payload parsing to src/util/hook-payload.ts, chunked to src/util/chunked.ts, and the embedding providers to the new src/embeddings/ folder. Every published name keeps its export from . and ./server. scripts/check-store-port.mjs gains storeFilesWithoutSql, a pinned list of src/store files that run no SQL, so a new pure helper filed there fails CI. CLI value imports from the store fell from 64 to 55.
  • Internal: CLI commands now read and write memory state through src/api operations instead of importing src/store or src/db. Runtime store imports in src/cli fell from 53 to 4, and the CLI no longer resolves the tenant per verb: dispatch resolves it once and passes it down.
  • Internal: The session-end work (handoff write, snapshot close, re-read booking) and the compact-resume read moved from the hook verb into src/api/session-end.ts.
  • Internal: scripts/check-layers.mjs now fails a runtime import from src/cli or src/cli.ts into src/store or src/db unless .cli-store-allowlist.json lists it with a reason. An entry that no longer matches an import also fails.
  • Internal: getHippoRoot and isInitialized moved from src/store/open.ts to src/core/project-identity.ts; both are path checks that never open the database.
  • Internal: no two top-level src/ folders import each other at runtime any more. scripts/check-import-cycles.mjs now fails CI on a folder-level cycle as well as a file-level one, with an allowlist that starts empty. The scope helpers moved to src/core/active-scope.ts, the project-fold reader to src/agent-memories/project-folds.ts, the error-to-status map to src/api/error-reply.ts, the merged-row rules to src/core, and the request deadline default to src/util/request-scope.ts.
  • Internal: MemoryKind and the kinds POST /v1/memories accepts come from one MEMORY_KINDS array in src/core/memory.ts, and the id-length cap MAX_ID_LEN lives in src/util/limits.ts instead of the HTTP helpers.
  • Internal: api, MCP and server code no longer reach into src/db. The pilot-arm write and the token-ledger skip rule (SQLite busy, corrupt and missing-table codes) moved into src/store, the server's held hippo.db connection and WAL checkpointer moved into src/store/sqlite/held-connection.ts, and StoreBusyError, isStoreBusy and STORE_BUSY_MESSAGE now live in src/store/port.ts beside the port that throws them. hippo-memory/server still exports StoreBusyError, and busy replies keep their 503 status and body. scripts/check-layers.mjs now fails a src/db import in src/api, src/mcp or src/server unless layers.json dbReach.allowed names it with a reason; what remains there is the request-store scope and the hippo.db fences.
  • hippo recall --help now lists --include-superseded, --as-of <iso-date> and --multihop. recall already read all three; its help left them out.
  • Internal: each CLI verb is one row in src/cli/verbs/: its handler, flags, help text and place in hippo help come from that row, and --dry-run support follows the flags it declares. A test now fails when a verb's help leaves out a flag it declares or names one it does not read; the flags still missing from help are listed in that test and may only shrink.
  • Internal: brief, note, policy, process and skill share one CLI handler set driven by a per-kind descriptor (src/cli/versioned-verbs.ts); notes, the entity graph, predictions and incidents moved to their own CLI files.
  • Internal: named list limit, dashboard port, ambient window and threshold constants, hour and day durations from util/time, and one truncate helper and JSON string guard replace repeated literals and copies.
  • Internal: GitHub and Slack now share one event-log vocabulary (githubEventRecord/slackEventRecord both answer { seen }, markGitHubEventSeen, markGitHubDlqRetried), one DLQ replay (replayParked) and one rememberWithEventLog; each connector injects only its signature check, envelope guard and re-ingest.
  • Internal: re-wrapped 52 src lines over 160 characters; runtime strings and patterns are unchanged.
  • Internal: Wrapped the last 24 source lines over 160 characters; no behaviour change.
  • hippo recall's ranking core writes through the store the caller passes. Its audit rows, goal log and strengthen now go to ctx.store when one is set, as every other recall path does; the second (global) store still gets its own row.
  • Internal: the store port no longer imports SQLite's busy codes. isStoreBusy moved to src/store/sqlite/busy.ts, beside the adapter that knows those codes.
  • Internal: the memory-quality audit rules (auditMemory, auditMemories) moved from src/store/audit.ts to src/core/memory-audit.ts, so the store file keeps only the audit-log SQL.
  • Internal: the graph writers take the caller's database handle only (insertEntityRow, updateEntityRow, insertRelationRow), so a write inside a rebuild transaction can no longer open its own connection and commit outside it.
  • Internal: choosing an embedding provider, reading its config and checking its API key moved out of the store into src/search/embed-memories.ts; the store keeps only the vector index identity. embedMemory and embedAll keep their exported signatures.
  • Internal: hippo reject and hippo invalidate reach the reject and invalidation flows through src/api (rejectOnHost, invalidateMemories).
  • Internal: goal commands use the shared host-admin guard; boost, clustering and recall-limit numbers and minute/hour durations are named constants; destructive verbs are flagged in the verb table.
  • Internal: Slack and GitHub dead-letter queues now share one insert, list, get and mark-retried module (src/store/connectors/dlq.ts); SQL behaviour is unchanged.
  • Internal: compaction replay steps take one options object and share one deadline-bounded loop.
  • Internal: hippo audit --fix and the sleep audit delete their error rows in one transaction after one read of the id set, with the same forget rows and reasons as before.
  • Internal: git learn builds the schema-fit tag counts once per batch instead of once per lesson; scores are unchanged.
  • Internal: the vector arm and a new physicsSearchIn take the store as a required argument; only the exported hybridSearch and physicsSearch still fall back to hippo.db under hippoRoot.
  • Internal: the list-limit rule (default, ceiling, integer check) lives once in src/util/limits.ts; the CLI and HTTP parseListLimit are adapters over it and the MCP tool schema imports the shared ceiling.
  • Internal: pct moved to src/util/format.ts; hippo card usage lines come from one table.
  • Internal: calculateStrength, calculateRewardFactor, netWrong and markRetrieved no longer read the environment; entry points pass strengthAblation() through DecayOptions.ablation. Default scoring is unchanged.
  • Internal: route tests read the live route tables instead of the route source text; the add-on and public-JSON boot checks moved to src/server/route-checks.ts.
  • Internal: the rollback-guard, dirty-flag migration, retry-delay and share-scrub timing tests now assert what their names claim.
  • Internal: the test suite sets HIPPO_SKIP_SCHEDULE=1 for every test, so no test can reach the real schtasks or crontab, whatever flags it passes. tests/scheduler-test-isolation.test.ts runs hippo init with no flags under a preload that logs and refuses every scheduler call, and a control run with the switch cleared shows the preload catches the calls. The test that creates a real Windows scheduled task now runs only on CI.
  • Internal: tests that asserted almost nothing now check behaviour. hybridSearch runs over stored vectors from a local hashed-embedding server; the tests check the 0.6 default blend weight and that MMR re-ranks only the top 100. isEmbeddingAvailable is checked true and false under a resolve-faking preload. The pragma-order churners report their writes and errors. The v26 guard test checks the healed schema, and hippo dag --stats is checked against a known fixture.
  • Internal: the coverage provider is now plain JavaScript (tests/_coverage-provider.mjs), so its tests run on the Node 22.16 leg too, which cannot load TypeScript.
  • Timing guard widened. tests/elapsed-time-upper-bounds.test.ts now flags any .ms, *Ms or *_ms name in an upper-bound matcher; the t.wait.ms < 5000 check in the Codex session test became a lower bound on the timeout.
  • No-throw-only checks replaced. The continuity self-heal, rollback guard and memory-value fit tests now assert the healed tables, the schema and floor meta, and finite in-range retention.
  • Tests assert behaviour, not source text or sleeps. The bearer lockdown test now sends a credential-less request to every row of the live route table, the curate CLI test runs hippo reject and hippo invalidate against a temp store, and fixed sleeps in eight tests became polls on a shared waitFor helper.
  • The elapsed-time guard now scans the dashboard tests too. tests/elapsed-time-upper-bounds.test.ts reads ui/src/**/*.test.ts(x) as well as tests/, so a wall-clock upper bound in a UI test fails the root suite.
  • The UI CI job runs its tests the normal way. No UI test asserts a time budget, so the unused UI_PERF_BUDGET_SCALE variable and the forced serial run are gone from ci.yml.

Fixed

  • Numeric CLI flags such as --min-mrr, --min-score, --importance, --days, --port and --limit now exit 1 with Invalid --<name>: "<value>". Must be a number. instead of turning junk into NaN. hippo eval --min-mrr abc used to pass its MRR gate.
  • A rule you state in a session is pinned when it is captured. Before, capture stored it like any other line, and the prompt hook brought it back only when a later prompt shared at least two of its words. A correction such as "never edit CHANGELOG.md by hand" was missing from a later task about rounding in src/money.js, so the agent made the same mistake again. Rules from the agent's own replies, and from --file or piped captures, stay unpinned, because those sources do not show that a person said them.
  • Context that Codex adds to a session no longer counts as something the user typed. Codex stores the AGENTS.md text, the environment block and app state as user messages. Capture and the session digest now skip them, so a rule from AGENTS.md is not saved as one the user stated.
  • Embedding failures now warn once per distinct provider and error, not once per process, so a later different failure is no longer hidden.
  • Outbound HTTP retries, an unexpected failure in the heartbeat auth check, a skipped heartbeat tick, an unreadable config.json, an unexpected repo-scan error, and an uncountable table now leave a log line.
  • Internal: unreadable embedding error bodies and config reads log at debug or warn, damaged dormant snapshots warn, and the graph page script reports an unparseable model on the console.
  • The GitHub backfill stops after 5 rate-limit retries or 10 minutes of waiting and fails with the reset time, instead of waiting without limit.
  • Internal: a failed ROLLBACK in a write scope is logged at error and no longer hides the original error.
  • Bad JSON in a ChatGPT or Claude import file, a malformed Codex history line and an unreadable server pidfile now log a warning before the fallback.
  • A browser request whose Origin is an https page on the server's own host is no longer treated as cross-site.
  • A test run now stops at once when node_modules does not match package-lock.json. With an older vitest installed (3.2 where the lockfile pins 5.0.3), vitest.config.ts ran once per project, the store guard ran twice and watched a different global store than the workers, and every run ended in a false "Test-isolation leak". The build-freshness check now names each drifted package and says to run npm ci.
  • An open that names no lock wait now waits 5 s everywhere. The journal-mode switch and migrations used to wait up to 30 s on a held lock while the open's own busy_timeout was 5 s, so a hook could spin far past its budget. One constant now sets both.
  • Sleep stops LLM extraction at the first failure that every later call would repeat. A bad or missing key, no credit, no permission or an unknown model (HTTP 401, 402, 403, 404) ends the pass and the DAG passes after it, and the sleep details say how many candidates were skipped. Rate limits and server errors keep the existing retry.
  • An LLM error reply now says why. The failure carries the first 256 characters of the error body on one line, with key-shaped text masked, so HTTP 401 reads HTTP 401: {"type":"error",...}.
  • One LLM call, retries included, now has a total time limit. It is two attempts plus the longest wait between them (128 s at the default 60 s timeout), where three slow failures could take three full timeouts.
  • A routed CLI write or hippo serve no longer bypasses a server that is too busy to answer its health probe. A probe timeout now fails with a message that names the server, its pid and HIPPO_HEALTH_PROBE_MS, where it used to fall back to opening the store as a second writer. A refused connection still clears the stale pidfile and falls back.
  • Internal: failures that left no trace are logged: a handle that fails to close after a failed open, an MCP call that fails after its timeout reply (was debug, now error), and a sleep child whose reply cannot be sent. A request deadline now runs every idle callback registered during a store call, not only the last.
  • A recall no longer waits on a local embedding model while it downloads. The first use of the local model can download its weights. Before, the recall waited for the whole download. Now the recall uses BM25 when its query-embedding deadline passes and says so once. The download goes on in the background, and later recalls in the same process use the model. A model that loads from disk is still waited for, because that load is short.
  • A model load that hangs now fails after 2 minutes. This applies to the local embedding model and to the cross-encoder reranker. A model that arrives after the timeout is still kept.
  • A failed model load is not tried again on every recall. After a failure, the next attempt waits 5 minutes. Before, the cross-encoder downloaded the model again on every recall after a failed load.
  • The warning for a failed model load now shows the real error. The warning line names the error and its class once in each 5-minute window. Before, the real error was logged only at debug level.
  • A blocked move of the old embeddings.json is now logged. When another process holds the file open, hippo logs the error and copies the file aside. Any other rename error leaves the file in place, and the next open tries the import again. If hippo cannot delete the original after a copy, it removes the copy, so copies do not pile up.
  • Internal: One module now finds and imports the optional Transformers.js package for both the local embedder and the cross-encoder. Both take the package as a parameter, so tests pass a fake one instead of mocking modules.
  • An MCP context without a role now fails closed. Only stdio, which passes no context, runs as the local operator; any other context runs as its own role, and one that omits it runs as a member with no host-admin rights. McpContext.role is now required, so a typed transport cannot forget it.
  • hippo_context no longer reads the server's git state for a remote caller. Over HTTP-MCP a caller who is not a host admin gets rows by strength instead of a query built from the server's working tree, and never the server's launch folder as its project. hippo_learn stays refused for those callers.
  • POST /v1/auth/keys checks the caller before it reads the body. An unauthenticated mint now gets 401 at once instead of costing a body read; the check runs again right before the mint, so an auth resolver's gate still sees no wait.
  • Internal: the /v1/outcome ids-cap and /v1/sleep comments now say what really guards each route (auth first; the host-admin check).
  • Legacy store files: an index.json or stats.json holding null, an array or a string no longer makes every open fail; it reads as empty with a warning. A bad legacy markdown file is skipped with a warning.
  • Index mirror: index.json is rewritten in place like stats.json.
  • Internal: package.json scripts from git history must be an object of strings, and a rejected body cancel between HTTP retries no longer loses the retry.
  • Failed auth is logged by default. A 401 or 403 now writes a warn line with the request id; other 4xx stay at info.
  • MCP errors are logged. A busy store logs at warn and a typed non-500 at info, both with the tool name.
  • Judge and consolidation failures are visible. Non-2xx and transport errors from the capture judge log at warn with status or error class; the sleep LLM passes record the error class and stack.
  • The OpenCode plugin reports a failed hook. A non-zero hippo session-end or last-sleep writes one line with the exit code to stderr; the hook stays non-fatal.
  • runWatched keeps a bounded stderr: it returns the last 64 KiB of a command's stderr instead of all of it, while the terminal still streams everything.
  • hippo graph view --open reports a launcher that will not start: it logs a warning and exits non-zero instead of staying silent.
  • One auth gate runs before every /v1 handler. Each row of the /v1 route table now declares its auth need (caller, admin, host-admin-local or webhook-signature), and dispatch enforces it before the handler runs. A request with a missing or bad key now gets 401 before any parameter check, where a few list routes used to answer 400 first. The Slack and GitHub webhooks are table rows that skip the key check because their signature check is the proof.
  • The MCP client key uses the real client address behind a trusted proxy. POST /mcp now keys per-client state on the same address the rate limiter uses, so both agree when HIPPO_TRUSTED_PROXIES lists the proxy. A forwarded header from an untrusted peer is still ignored.
  • The GitHub webhook refuses a replayed delivery. A signed delivery whose X-GitHub-Delivery id was seen in the last 24 hours gets 409. A delivery that failed releases its id, so GitHub's redelivery still gets through.
  • Internal: PUBLIC_ROUTES and the exported subscriberKey are gone; tests read the public rows off V1_ROUTES and test subscriber keys through clientLimitKey.
  • Embedding failures after the first no longer vanish: a repeat failure is counted and reported as one warn line at most once a minute.
  • A failed /health probe no longer deletes the pidfile unless it proves no server is there: a refused connection or a non-JSON body still clears it; any other error keeps it and logs a warn.
  • runWatched has a 30 minute deadline: a hung transfer command is killed and the call rejects with RunWatchedTimeoutError; an optional second argument sets the deadline.
  • The store executor logs a worker that fails to terminate instead of dropping the rejection.
  • The thin client rejects a 2xx reply that is not a JSON object with a MalformedReplyError naming the route.
  • A sleep whose rescue audit cannot open the store now logs a warning with the error class. It was a debug line, so the missing audit rows went unnoticed.
  • Internal: the auto-share threshold is one named constant shared by sleep, hippo share --auto and autoShare.
  • hippo process new and hippo process supersede report a refused save like every other kind. A save the store refuses (an empty step, a superseded predecessor) now prints the store's message alone and exits 1, as brief, note, policy and skill already did, instead of escaping as an uncaught Error: ....
  • The sharing-scrub speed test no longer fails at random on busy runners. It used to divide the time for a 1 MB input by the time for a 64 KB one, and the small reading was under a millisecond, so timer noise could push the ratio over its limit. It now compares one 1 MB scrub with sixteen 64 KB scrubs of the same bytes. Linear work reads near 1 and quadratic work near 16, and the limit is 4.