v1.38.7
1.38.7 - 2026-09-06
Fixed
hippo forget --archivesilently 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", butPOST /v1/memories/:id/archiveandclient.archiveRawhad existed sinceea155d6(2026-04-29); the comment was written three weeks later in29d96d8(2026-05-21) and never checked against the code it described. Because archive skippedrunViaServerIfAvailable,HIPPO_REQUIRE_SERVER=1was also silently bypassed: with a server up and required,hippo forget X --archivestill wrote directly instead of erroring like plainforgetdoes. Archive now routes through the existing endpoint the same way plain forget already does;--reasonis 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, notcli. The routed path builds its actor from the request (buildContextWithAuth) instead ofapi.adminActor('cli'), so a server-upforget --archivenow recordslocalhost:cliinraw_archive.archived_byand thearchive_rawaudit row, matching every other routed command. This is a visible provenance change for anyone reading that column, not a bug. hippo forgetcould not fall back when a server died mid-command.runViaServerIfAvailablehas a connection-refused branch that clears the stale pidfile and retries direct, andrememberhas always used it. Theforgetdispatch caught every routed error itself and exited first, so that branch was unreachable forforgethowever the server went away: a server that passed the/healthprobe and then stopped left the user withfetch failedand 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.
isConnectionRefuseddecided whether a routed request had failed at the transport by substring-matching the error message foreconnrefusedandfetch failed, but the server echoes the caller's id and content back in its error text.hippo forget mem_ECONNREFUSEDagainst 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 distinctHttpResponseErrorand are never classified as transport failures. This affects every routed command, not justforget: the same message could be reached throughrememberwith matching content. - Routed archives stopped counting toward
total_forgotten. The counter was incremented in the CLI's direct path only, so once--archivebegan routing,hippo statsundercounted every archive taken while a server was running.updateStatsmoved intoapi.archiveRaw, which both the direct path and the HTTP route call, so the count no longer depends on which path ran. Plainforgetstill counts only on its direct path; that is unchanged and tracked inTODOS.md.
1.38.6 - 2026-09-06
Fixed
- Sleep was overwriting the stored confidence tier with a time-derived guess, silently upgrading
inferredmemories toobserved. Reproduced on a real store: aninferredentry 40 days stale went tostaleon disk after oneconsolidateand thenobservedafter onemarkRetrieved, with nothing left to say it had ever beeninferred. Root cause wassrc/replay.ts:104reading eligibility off the storedconfidencecolumn instead of deriving it, which forcedsrc/consolidate.tsto persistresolveConfidence'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 callsresolveConfidencedirectly; consolidate persists onlystrengthand leavesconfidenceas 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'sstale->observedmapping on recall now only fires for rows deliberately markedstaleby 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 contextloaded every entry twice to feed them.api.getContextalready loaded, superseded-filtered and admission-filtered every local and global entry to build the injected set, butcli.ts's ambient landscape summary re-derived all four steps itself (its ownloadConfig, its ownresolveProjectIdentity, its own copy of the admission predicate, a secondloadAllEntriesper store) instead of reading whatgetContextalready computed. The CLI's own comment recorded that these two copies had already drifted once (codex P2-13).getContextnow returns the computedambientStateon 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 interactivehippo context. TheUserPromptSubmithook path (--pinned-only --format additional-context) never paid this cost: it is gated by!pinnedOnlyand 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 thatgetContextperforms, soavgStrengthcontinues 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 lockedon 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.openHippoDbranPRAGMA journal_mode = WALbeforePRAGMA busy_timeout = 5000(src/db.ts:2366-2367).journal_modeis 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 instantdatabase is locked"despitebusy_timeout" from the 1.38.3 Tests note. Separately,runMigrationsended with 7 no-opINSERT OR IGNOREstatements inensureMetaDefaultsand a same-value meta upsert inensureOptionalFts, 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-keySELECTthat returns before any write, and a single-key compare before the fts5 flag write. TheCREATE ... IF NOT EXISTSself-heal andbackfillFtsIndexare untouched, so a droppedmemories_ftsstill 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_timeoutmoved 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 concurrentBEGIN IMMEDIATEmade the old open burn the full 5000 ms and throw (3 of 3 at 5500.6, 5512.4 and 5493.0 ms). -
hippo session-endcould exit before its sleep write landed.cmdSleepis async, butcmdSessionEndWorkerand its codex twincmdCodexSessionEndWorkerwere typedvoidand called it withoutawait, so a rejection escaped thetry/catchand the process could exit mid-write. Both workers andcmdSessionEndare now async,cmdCodexRun's exit handler awaits its inline fallback before theprocess.kill/process.exitcalls 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.tsruns two real writer processes against the store while callingopenHippoDbin a loop and asserts it never throws (red 3/3, green 3/3).tests/db-open-write-free.test.tsholdsBEGIN IMMEDIATEon a second connection and bounds the open at 1000 ms (was a throw at 5558 ms, now returns in 152 ms), and pins that droppingmemories_ftsstill rebuilds and backfills on reopen.tests/session-end-worker-await-sleep.test.tsforces a sleep failure and asserts the worker still finishes in order (was status 1, now 0).