Skip to content

v1.38.9

Choose a tag to compare

@kitfunso kitfunso released this 07 Sep 13:56
· 205 commits to master since this release
3d723d5

1.38.9 - 2026-09-07

Fixed

  • hippo forget counted toward total_forgotten only when the command missed the server. The dispatch routes through runViaServerIfAvailable when hippo serve is up (src/cli.ts:9147), but the counter lived in the CLI's direct path and api.forget incremented nothing, so the same command moved the number or not depending on whether a server happened to be running. Anyone running hippo serve full time saw a counter that tracked only the forgets that missed it. v1.38.7 fixed one instance of this class by moving the archive counter into api.archiveRaw; this is the plain-forget half. api.forget now counts and src/cli.ts no longer does, because that function's only two callers (src/cli.ts:3698 and the HTTP route at src/server.ts:1089) are the two paths of one user command, so no surface can miss it. Pinned by a real-server test that runs a routed pair and a direct pair and asserts the recorded actor on each, so it cannot pass by taking the direct path twice.
  • The comment above the thin-client routing filter claimed salience gates need the direct path. They do not. richFlag (src/cli.ts:8808) has no salience condition, and api.remember has no gate, so a routed remember stores what a direct one would skip. Measured on a real spawned server with salience.enabled: true: a duplicate is Skipped (100% overlap) with no server up and Remembered twice with one up. Same shape as the v1.38.7 archive bug, a comment describing behaviour nobody re-checked. The comment now states the bypass; the bypass itself is a product call and is filed in TODOS.md.
  • hippo stats counters lost increments under concurrency, and a write to one counter could roll back another. updateStats (src/store.ts:2316) read all three meta counters, added the delta in JS, and wrote all three back, with no transaction. Two writers interleaving between the read and the write lost one increment, and because the two counters the caller never named were written back too, a stale total could be stamped over another process's committed value. Reproduced with real node processes before the fix: four workers doing 40 increments each counted 140 of 160, and two workers incrementing different counters left one at 52 of 60. Both binds are the same string on purpose: node:sqlite binds a JS number as REAL, which stores "1.0" into this TEXT column. Each counter is now a single atomic INSERT ... ON CONFLICT DO UPDATE SET value = CAST(meta.value AS INTEGER) + CAST(? AS INTEGER), applied only to the counters the delta names. No call site changes: all nine increment sites route through this one writer. bootstrapLegacyStore (src/store.ts:1128-1130) also writes these keys, but it sets absolute values once on a cold legacy store, so it is not part of this race. Per-counter atomicity is sufficient because the only consumer reads the three independently (src/cli.ts:3539-3541).

remember was attempted in this release and reverted before merge. There is no correct place for its counter yet: api.remember is the bulk write path for the Slack and GitHub connectors and for import, and the POST /v1/memories route is not a user surface either, since deploy/aml/adapter/adapter.mjs posts to it per leaderboard add and the Python SDK is a general client on it. Counting there would also make two paths that store different things report the same number. Recorded in TODOS.md behind the salience item. Two related gaps stay open there too: an HTTP-only recall does not count, and no MCP operation counts.

Known limitations

  • Two processes opening a store that does not exist yet can still crash with database is locked (errcode 517, SQLITE_BUSY_SNAPSHOT) inside runMigrations, which busy_timeout does not retry. Found while building the test for this fix and recorded in TODOS.md; it needs its own reproduce-and-measure pass. Warm stores are unaffected, because runMigrations short-circuits once the schema version is current.