Skip to content

v1.38.7

Choose a tag to compare

@kitfunso kitfunso released this 06 Sep 20:49
· 100 commits to master since this release
eba2eac

1.38.7 - 2026-09-06

Fixed

  • hippo forget --archive silently dropped out of server routing. The dispatch's own comment claimed "the HTTP forget route does not carry it, so archive requests always take the direct path", but POST /v1/memories/:id/archive and client.archiveRaw had existed since ea155d6 (2026-04-29); the comment was written three weeks later in 29d96d8 (2026-05-21) and never checked against the code it described. Because archive skipped runViaServerIfAvailable, HIPPO_REQUIRE_SERVER=1 was also silently bypassed: with a server up and required, hippo forget X --archive still wrote directly instead of erroring like plain forget does. Archive now routes through the existing endpoint the same way plain forget already does; --reason is validated before routing so the error is identical whether or not a server is up.
  • Archiver provenance on the routed path is now localhost:cli, not cli. The routed path builds its actor from the request (buildContextWithAuth) instead of api.adminActor('cli'), so a server-up forget --archive now records localhost:cli in raw_archive.archived_by and the archive_raw audit row, matching every other routed command. This is a visible provenance change for anyone reading that column, not a bug.
  • hippo forget could not fall back when a server died mid-command. runViaServerIfAvailable has a connection-refused branch that clears the stale pidfile and retries direct, and remember has always used it. The forget dispatch caught every routed error itself and exited first, so that branch was unreachable for forget however the server went away: a server that passed the /health probe and then stopped left the user with fetch failed and an untouched memory. Transport failures now reach the shared handler; application errors are still reported in place.
  • A server error quoting your own id could tear down a healthy server's pidfile. isConnectionRefused decided whether a routed request had failed at the transport by substring-matching the error message for econnrefused and fetch failed, but the server echoes the caller's id and content back in its error text. hippo forget mem_ECONNREFUSED against a live server returned an ordinary 404, was classified as a dead socket, and the fallback deleted the running server's pidfile, so every later command silently stopped routing. Errors raised from an HTTP response are now a distinct HttpResponseError and are never classified as transport failures. This affects every routed command, not just forget: the same message could be reached through remember with matching content.
  • Routed archives stopped counting toward total_forgotten. The counter was incremented in the CLI's direct path only, so once --archive began routing, hippo stats undercounted every archive taken while a server was running. updateStats moved into api.archiveRaw, which both the direct path and the HTTP route call, so the count no longer depends on which path ran. Plain forget still counts only on its direct path; that is unchanged and tracked in TODOS.md.

1.38.6 - 2026-09-06

Fixed

  • Sleep was overwriting the stored confidence tier with a time-derived guess, silently upgrading inferred memories to observed. Reproduced on a real store: an inferred entry 40 days stale went to stale on disk after one consolidate and then observed after one markRetrieved, with nothing left to say it had ever been inferred. Root cause was src/replay.ts:104 reading eligibility off the stored confidence column instead of deriving it, which forced src/consolidate.ts to persist resolveConfidence's time-derived value back to disk on every sleep at three sites, destroying the stored tier for any entry that aged past 30 days. Replay now calls resolveConfidence directly; consolidate persists only strength and leaves confidence as stored. This reverses the confidence half of review round P2-1 (the strength half, refreshing stored strength on every survivor, is untouched and still correct). src/search.ts:1263's stale -> observed mapping on recall now only fires for rows deliberately marked stale by invalidation or already flattened by a pre-fix sleep; those rows cannot be recovered, because the tier is already gone from disk. No export or signature change.

1.38.5 - 2026-09-06

Fixed

  • The v39 ambient admission policy lived in two hand-synced copies, and hippo context loaded every entry twice to feed them. api.getContext already loaded, superseded-filtered and admission-filtered every local and global entry to build the injected set, but cli.ts's ambient landscape summary re-derived all four steps itself (its own loadConfig, its own resolveProjectIdentity, its own copy of the admission predicate, a second loadAllEntries per store) instead of reading what getContext already computed. The CLI's own comment recorded that these two copies had already drifted once (codex P2-13). getContext now returns the computed ambientState on its one entry-carrying return, and the CLI renders it instead of re-loading. Measured on this machine's two real stores before the fix: 508 rows local and 1818 global, each loaded twice, roughly 40 ms per interactive hippo context. The UserPromptSubmit hook path (--pinned-only --format additional-context) never paid this cost: it is gated by !pinnedOnly and the additional-context branch never called the summary at all, so this is not a hot-path fix. The summary is still computed after the retrieval marking that getContext performs, so avgStrength continues to reflect post-retrieval strength exactly as the CLI-side version did; moving the computation across that mutation changes the rendered strength label, and a regression test now pins it.

1.38.4 - 2026-09-06

Fixed

  • database is locked on ordinary opens had two independent causes, and the one named in the 1.38.3 follow-up was not either of the ones we had guessed. openHippoDb ran PRAGMA journal_mode = WAL before PRAGMA busy_timeout = 5000 (src/db.ts:2366-2367). journal_mode is the first lock-taking statement of every open, so it ran at SQLite's default busy timeout of 0, got no busy handler, and threw instantly instead of waiting. That is the instant database is locked "despite busy_timeout" from the 1.38.3 Tests note. Separately, runMigrations ended with 7 no-op INSERT OR IGNORE statements in ensureMetaDefaults and a same-value meta upsert in ensureOptionalFts, so every open took a RESERVED write lock even on an already-current store and any concurrent writer could make an unrelated read command fail to open. Both are now read-first: one multi-key SELECT that returns before any write, and a single-key compare before the fts5 flag write. The CREATE ... IF NOT EXISTS self-heal and backfillFtsIndex are untouched, so a dropped memories_fts still rebuilds and backfills.

    The two fixes close different paths and neither closes the other's. Measured with a 20 Hz open/close poller against sequential writers, 4 reps per arm: with the churn poller and the per-open write, 4 of 4 reps crashed; with the write removed, 3 of 4 still crashed; with no churn, 0 of 4; with busy_timeout moved first and the write kept, 0 of 4. So removing the per-open write does not fix the churn path, and the reorder does not fix the held-lock path. The reorder trades no stall for the crash: max open 30.7 ms over 2696 opens, zero opens above 50 ms, writer p95 8 ms. The write-free open is justified on its own path, where a concurrent BEGIN IMMEDIATE made the old open burn the full 5000 ms and throw (3 of 3 at 5500.6, 5512.4 and 5493.0 ms).

  • hippo session-end could exit before its sleep write landed. cmdSleep is async, but cmdSessionEndWorker and its codex twin cmdCodexSessionEndWorker were typed void and called it without await, so a rejection escaped the try/catch and the process could exit mid-write. Both workers and cmdSessionEnd are now async, cmdCodexRun's exit handler awaits its inline fallback before the process.kill/process.exit calls that used to cut it off, and the three dispatch sites await. Closes both TODOS.md follow-ups named in the 1.38.3 Tests note.

Tests

  • Three new real-DB files, each confirmed red before its fix and green after. tests/db-open-pragma-order.test.ts runs two real writer processes against the store while calling openHippoDb in a loop and asserts it never throws (red 3/3, green 3/3). tests/db-open-write-free.test.ts holds BEGIN IMMEDIATE on a second connection and bounds the open at 1000 ms (was a throw at 5558 ms, now returns in 152 ms), and pins that dropping memories_fts still rebuilds and backfills on reopen. tests/session-end-worker-await-sleep.test.ts forces a sleep failure and asserts the worker still finishes in order (was status 1, now 0).