You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Add the Z0 set X runner, where each lesson's apply tasks run in Codex.ab-run.mjs now runs the four Codex arms of prereg 85: X1 (built-in memory), X2 (hippo in Claude Code and Codex on one store), X3 (a stub that imports AGENTS.md) and X4 (the taught lessons written into AGENTS.md). Teach tasks stay in Claude Code. Each apply runs codex exec in a per-run Codex home, reads usage and turns from Codex's rollouts, and runs the G1 read check over them. X2 installs hippo for Codex inside each run and waits for the end line of hippo's session-end worker before the surface snapshot. If the install changes your own Codex launchers, the runner puts them back and abandons the run. Your Codex login is copied once and sits in a run only while a session runs; a run killed by Ctrl-C can leave the copies behind. A file the run writes that holds the login is deleted, and the run stops. The four slow Codex test files run in the token-eval workflow. No hippo default changes.
Add the Z0 G5 regrade, the reader sample and the stored sample.scripts/token-eval/z0-regrade.mjs runs every saved lesson check and acceptance test again on the saved commits (prereg 166), records each verdict change as a flip, and writes the grading.json that z0-analyze.mjs reads. A failed harness gate is an error row that the next run retries, and a stopped regrade resumes. The reader mode draws a seeded, blinded sample of diff and lesson pairs and scores the labels; the stored mode does the same for the prereg 179 check of the stored judgement and reports agreement, a Wilson interval and kappa. grade.json now records whether the final check ran. z0-analyze.mjs prints the stored-sample line when grading.json holds it. A checker's identity is its entry script, that script's extension, the local files it imports and its args. A moved results folder reads as unchanged. The harness reads imports with the typescript parser and stops the load on an import it cannot hash. A reader round that passes freezes the checkers: regrade --post-fix and grading refuse a checker that the round did not judge. grading and each reader draw also refuse post-fix rows from two versions of one checker. A scored reader round or stored sample is final: the score command refuses other labels for it. No hippo default changes.
The delivery ledger now records session ends and agent-run hippo context calls. With deliveryLedger.enabled on, hippo session-end writes one boundary row for a host's SessionEnd payload, after its background worker has started, so a locked store never delays capture. A manual run and a --turn call (the end of one reply) write no row. hippo context without --pinned-only, the call the instruction block asks an agent to run, writes one context row that lists the memories it returned and the ones its limit cut. The delivery ledger stays off by default, and what both commands print, save and exit with is unchanged.
A context row links the recall trace its call wrote. When the call returned memories, recall_trace_id names that trace and query_hash equals the trace's own query hash, so a reader can join a delivery to its ranked results. A call that returned nothing keeps the query hash and links no trace id. The per-prompt hook writes no trace, so its rows keep both fields empty.
Changed
Internal: the size, comment-history and store-port ratchets now hold at the lower counts the comment sweep reached, and the architecture notes name the summary-dirty helper that replaced a deleted one.
Internal: CI now fails when a promise call in src/ is neither awaited nor handled. Since CLI verbs throw to exit, a dropped await would turn a clean exit into an unhandled rejection.
Internal: Cut the comments in src/store/ to the two-line rule (banners removed, long blocks shortened, stale file pointers fixed). Comments only, no behaviour change.
Internal: Comments in src/api, src/server, src/core, src/mcp, src/search and src/rerankers now follow the two-line rule; two long essays moved to docs/ARCHITECTURE.md. Comments only, no behaviour change.
Internal: Cut the comments in src/cli/, src/db/, src/util/, src/hooks/, src/consolidate/ and the top-level src/*.ts files to the two-line house rule: banners removed, long blocks shortened, stale file pointers fixed. Comments only, no behaviour change.
Internal: cut comments in the objects, connectors, capture, graph, importers, sharing, eval, learn, predictions, trust and entry source folders to the two-line rule. Comments only, no behaviour change.
delivery_events.ledger_version is now 3. It means a binary that can write session-end and context rows wrote the row, so event_type has six values and surface can be context. There is no schema change.
Internal: the CLI's server client reads every reply under a 1 MiB cap. If the port in the pidfile now belongs to another process, a huge reply ends in an error instead of filling memory.
Internal: The test-only export check no longer counts a name in a src/ comment as a production use. The 16 exports this exposed are resolved at the source: dead duplicates are deleted in favour of their production twins, module-private helpers lose export, and their tests now run through the public entry points.
Internal: the embedding index and the promote check no longer open the database themselves. src/store/embeddings/index.ts and src/api/promote.ts call named functions in src/store/vector-index.ts (the renamed src/store/vector-writes.ts, which now holds the index reads too) and src/store/tenant-lookup.ts; behaviour and the published exports are unchanged.
Internal:hippo embed on a store with no backend now lists memory ids and reads the rows 64 at a time, for both the backfill and a model-change rebuild, instead of loading every memory; the rebuild still replaces the whole index in one transaction. Completing a card now promotes its unblocked children with one read and one update instead of up to four statements per child.
hippo status skips the all-pairs physics energy above 2000 particles and prints energy: skipped (<n> particles).
hippo provenance and hippo correction-latency read only the rows their report needs instead of every memory.
Internal: the sleep conflict refresh reads only rows that hold conflicts or are named, and the physics force loop accumulates in place instead of allocating per pair.
Sleep: a sleep run now reads the store twice instead of three times and keeps no deep copy of it; on a 20,000-row store the dry run's peak memory fell from 406 MB to 387 MB (median of five), and its time stayed flat at about 16 s.
hippo status now counts its rows in one pass over ten narrow columns instead of loading every memory, so it reads no memory text and builds no row array. The printed text is unchanged.
hippo status now counts stored vectors from their ids and one blob length instead of decoding every vector, so on a synthetic store of 20,000 vectors at 384 dimensions the read fell from a median 301 ms to 14 ms. The printed text is unchanged.
Internal: the CLI now resolves its tenant once per command and builds its api context in one function (cliApiContext), instead of repeating both across the verb files. No verb reads a different tenant, writes a different audit actor or prints different text. A new tenantResolvesInCli counter in scripts/check-store-port.mjs reports the remaining direct tenant lookups.
Internal:hippo watch, hippo audit --fix, hippo refine, hippo decide --supersedes and the session-end re-read count no longer call the store's writeEntry, deleteEntry or openHippoDb from src/cli/. watch stores its failure through api.remember. audit --fix and the sleep audit share removeAuditErrors in src/api/audit.ts. refine stores a rewrite through storeRefinement in the new src/api/refine.ts. decide --supersedes weakens the old memory through weakenSupersededMemory in src/objects/decisions.ts. recordRereads in src/store/token-ledger.ts now takes the store root and opens it. scripts/check-cli-recall-writes.mjs fails when a CLI file names a memory writer or the store opener. It names three files that still do, each with its reason: quality-repair.ts, projects.ts and sleep.ts. Stored rows, audit rows, printed output and exit codes are unchanged, which tests/cli-maintenance-writes-parity.test.ts pins.
Internal: a CLI command that stops early now throws one typed error (CliExit) and runCli is the one place that turns it into the exit code, so every caller's cleanup runs on the way out. Output, streams and exit codes are unchanged. A new CI check, scripts/check-process-exit.mjs, fails on a process.exit call in src/ outside a short list of files.
Internal: CLI verbs that read one memory by id now go through the api, and the store-port check counts CLI value imports from the store.
Internal:hippo recall now reads the store, the search helpers, the reranker registry and the global store through src/api/recall-cli.ts, and takes its tenant from the dispatch context. No output, exit code or written row changes.
Internal: each CLI verb is stated once in the verb table; its help blocks and flags are looked up by the row key, and the compiler checks the key sets agree.
hippo supersede is now one transaction. The new row, the link on the old row and the audit row commit together, so a failure part way leaves neither a successor without a link nor a link without a successor. The audit log gets two rows per supersede where it got three: remember for the new row and supersede for the old row. The second remember row, which named the old row, is gone.
hippo supersede no longer changes a personal row. The CLI has no owner, so an id in someone's personal:private: scope now answers Error: memory <id> not found. with exit code 1, the same answer hippo forget and the HTTP and MCP routes give. Before, the CLI wrote the successor into that person's scope.
Internal:hippo remember, hippo supersede and hippo trace record write through api.remember and api.supersede in place of the store's writeEntry. RememberOpts gains an optional local group (layer, pin, confidence, source, schema fit, trace outcome, source session id, and a salience gate's start-weak verdict) and api.supersede gains an optional fourth argument (layer, tags, pin). Both are for a caller in the same process: POST /v1/memories, POST /v1/memories/:id/supersede and the MCP hippo_remember tool do not read them from a client. Stored rows, the remember audit rows, the printed output and the exit codes of the three verbs are otherwise unchanged, which tests/cli-write-verbs-parity.test.ts pins.
Internal:memory_conflicts reads in src/store/conflicts.ts share one column list, and the recall and context tool descriptions read their defaults from the code constants.
Internal: the GitHub and Slack dead-letter stores now share one naming scheme, every Github identifier is spelled GitHub, and the GitHub CLI verb moved from src/connectors/github/cli-impl.ts to src/cli/github.ts. No behaviour change.
Internal: saveCompaction and replayCompactions in compaction-record.ts are split into named steps, and each save failure is reported once through one helper.
Internal:hippo serve and serve() take the default port (6789) and host (127.0.0.1) from one module, so the CLI and the library cannot drift apart.
Internal: the goal-stack boost (boostByGoals and its helpers) moved from src/store/goals.ts to src/search/goal-boost.ts, because it ranks rows and touches no store. No behaviour change.
Internal: the cursor, openclaw, opencode and pi instruction blocks now come from one template function with byte-identical output, the local embedder's pipeline has a typed interface instead of any, and the default context budget is one exported constant.
Internal:src lines over 160 characters are now counted by the size ratchet and wrapped, so the number can only fall. No behaviour change.
Internal: The CLI code has 68 fewer old lint hits, and the whole-repo count falls from 91 to 23. Casts now carry a SAFETY: line or are gone, named guards replace typeof checks, and optional JSON keys are set in the same order as before. No command output changes.
Internal: cleared 46 old lint hits outside the CLI (config spreads, vector builds, regex escapes, type guards in the AML adapter); no behaviour change.
Internal: Cleared twelve oxlint hits in usage text, CLI audit and learn helpers, session event types, and three benchmark scripts, with no behaviour change.
Internal: The last CLI flag casts are typed (flag list and member helpers), so the lint count is 0; hippo supersede --layer now rejects an unknown layer, and a bare hippo invalidate --reason stores no reason.
Internal:hippo remember and hippo watch now run one shared local write pipeline in the api (rememberLocally: schema fit, the salience gate, the write, the read-back, the remembered counter and the embedding), and hippo remember asks the api for fact extraction (extractRememberedFacts). Stored rows, counters, printed lines and exit codes are unchanged; a new spawned-CLI test pins them.
Internal: The core memory model now names its aged-out threshold, retrieval half-life gain and error-tag half-life factor, the SQL strength rule reuses the shared fallback half-life, and the default tenant has one constant (DEFAULT_TENANT_ID); comments in src/core/memory.ts are cut to the house rule. No behaviour change.
The typed-object routes no longer run SQLite on the server thread. The 38 /v1 routes for decisions, incidents, processes, policies, skills, project briefs and customer notes now do their store work on the store worker threads, so a slow object query no longer stalls other requests. No API change: same status codes, bodies and audit rows.
Internal: every CLI verb now enters through one shape, handle<Verb>(ctx), and cmd<Name> names only a typed function below an entry. No verb's output, writes or exit codes change.
Internal: The memory column list, the process step caps and the 4096-character field cap are each written once and read from there; SQL, error text and status codes are unchanged.
Internal: The memory quality repair now lives in src/store/ and opens its unmigrated handle through withUnmigratedDb in src/db/open.ts, so no CLI file opens hippo.db or sets its own PRAGMAs.
Internal:recall() now reads and writes through the same store method names and argument object as retrieve(), the physics-or-hybrid ranking choice is one function, and the pass-through buildSuppressionSummary helper is gone (callers build the RecallSuppressionSummary directly). Results, rows written and audit rows are unchanged.
Internal: what a recall records after it ranks (the anchor-skipped audit row, the session ring, the recalled counter and the token ledger row) is now written once, in src/api/recall-finish.ts, for GET /v1/memories and hippo recall. The HTTP route passes recordAs to retrieve and the CLI verb no longer feeds its ring. RecallOpts gains the optional recordAs and ShownCliCore the optional anchoredOn. No surface records anything different; MCP still records its own and is next. scripts/check-store-port.mjs now fails when a route or CLI file records a recall itself.
Internal: The graph, prediction and typed-object routes now reach the store through src/api functions that take the request Context, and check-layers fails any file under src/server/routes/ that names requireGroup or storeFor.
Internal: the SQL twins of the strength and keep rules (strengthSql, AUTO_DELETABLE_SQL) moved from src/core/memory.ts into src/store/rule-sql.ts. No behaviour change.
Internal: the scripts now share one seeded random generator (scripts/lib/prng.mjs) and one comment stripper (scripts/lib/source-text.mjs) instead of pasted copies. No behaviour change: every copy was checked to draw the same sequence or give the same output first.
Internal: The long functions in the E1 driver, the physics ablation, the benchmark seed set, the soak test, the comment-history check and the model-profile bench are split into short named functions. Every script prints and writes what it did before.
Internal: split src/sharing/shared.ts into global-store.ts, search-both.ts, share.ts and global-sync.ts by concern. Code moved unchanged; no behaviour change.
Internal:scripts/z6-supersession-eval.mjs is 584 lines instead of 1,499. Its fixture contract, scoring, output parsers and selftest moved to four modules under scripts/z6-supersession/; the runner, its pre-flight guards and every flag stay in the entry file. runScenarioArm hands its label step to labelScenarioArm and is 73 lines instead of 95. No behaviour change: fixture ids, the split, selftest output and recorded hashes are the same.
Internal: store modules now open a handle through onHandle instead of hand-written open/try/finally/close blocks, and predictions.ts keeps its prediction column list in one constant.
Internal: every unpublished test seam is now named _<verb><Thing>ForTests, and check-test-only-exports fails a new seam that drifts from that scheme.
Internal: deleting a raw row now fails with a typed RawAppendOnlyError that the CLI and the dashboard test with instanceof, in place of matching the error text. Printed lines, exit codes and status codes are unchanged.
Internal: the three agent-eval tests that run the learning simulation get a 30 s budget, so a loaded Windows runner no longer fails them at the 5 s unit default.
Internal: 22 tests that ended on an assertion no bug could break now assert the behaviour their name states (recall output, stored row fields, returned values). One recall test queried a word that matched nothing, so it passed on an empty result.
Internal: six embedding tests (lifecycle-stress, global-row-embeddings) now report as skipped, not passed, when the local embedding backend is missing, and the micro-eval CI job runs them with HIPPO_REQUIRE_EMBEDDINGS=1 so a missing backend there fails.
Internal: the floating-promise check tests get a 30 s budget, since the TypeScript checker's cold start passed 5 s on a loaded runner.
Internal: two rate-limit tests now stop the clock while they count tokens, so a slow Windows runner can no longer refill a bucket between requests and turn the expected 429 into a 200.
Internal: cleared the 64 older lint hits under tests/: 23 type assertions deleted, 6 given a SAFETY: line, and no assertion removed.
The VS Code and Copilot capture hooks now have store-level tests for a busy store, a killed worker, two sessions at once and a save from the wrong folder. The tests run the built CLI against a real store. docs/integrations/agent-inventory.md lists what they cover, and three limits found while writing them. Internal only, no runtime behaviour changes.
Internal: The scheduler tests now run on every platform against real temp folders, and the filesystem test seam in src/cli/scheduler.ts is gone. Two fresh-tail recall cases now assert the rows returned, and two log tests prove an absence with a positive control instead of a fixed sleep.
Fixed
hippo serve now always exits after a crash or a SIGTERM / SIGINT. A store thread stuck in a long statement could keep a stopped server alive for ever, so a supervisor never restarted it. Shutdown now has a time limit: 10 s past the request drain window (15 s by default). At the limit the server logs one error line and ends with a failure exit.
A hippo mcp request over stdio now has the same deadline as an HTTP request. A call that never finished left the client waiting with no reply. After 120 s (or HIPPO_REQUEST_DEADLINE_MS, where 0 turns it off) the client gets one JSON-RPC error with code -32001 that names the call, and a reply that arrives later is dropped.
Hippo reads a file through the same open handle it checked. Agent memory files, session transcripts, a .git worktree file and log tails in a support bundle were sized or tested by path and then opened again by path; a file swapped in between could be read past the size cap or as the wrong kind. Results for ordinary files are unchanged.
A FIFO in an agent memory folder is skipped as "not a file" straight away instead of being able to hold the import waiting for a writer.
The pre-compact diagnostic log checks its size on the handle it writes through. It still starts again once it passes 256 KB. Two hooks logging in the same instant can now overwrite one diagnostic line.
hippo eval --suite reads its baseline in one step. A missing baseline is still silent and an unusable one still warns.
Error logs in catch blocks now carry the error class and stack. Twelve more log.error calls (auto-sleep, daily runner, summary rebuild, dashboard, db open, mirror cleanup, recall trace, quarantine) pass the same fields the rest of the repo logs.
A damaged steps column on a process now warns. It still reads back as no steps, but the log names the table, row and column, as the linked_memory_ids column already does.
A read that fails inside a snapshot keeps its own error. If the closing commit also failed, the caller used to see the commit error instead of the read error.
A store rebuilt from its markdown mirror now keeps which memories were superseded, when each became valid, where each was extracted from, and its DAG level and parent. Before, the mirror files left these out, so a rebuild brought superseded memories back as current.
A new Codex launcher now points at a file that exists. Since 1.61.0, hippo hook install codex and hippo setup wrote a launcher that named a missing file, so codex did not start. hippo also stopped putting its launcher back after a Codex update replaced it. The upgrade does not rewrite a launcher that 1.61.0 to 1.70.0 wrote: run hippo hook uninstall codex, then hippo hook install codex.
hippo dashboard now serves the UI the package ships. In 1.70.0 it looked for the UI in the wrong folder and showed the "not built" page.
A recall no longer hangs on a slow embedding service. A recall that needs a query vector from a remote provider (openai, voyage, cohere) now waits at most 5 seconds for it in total, across every retry, then answers from keyword search alone and logs one warn line that names the provider and the budget. Before, three attempts of 30 seconds each could hold a recall for over 90 seconds. HIPPO_QUERY_EMBED_TIMEOUT_MS changes the budget. The in-process local provider has no budget, because nothing can stop it once it starts. Storing a memory keeps its long timeouts. An embedding request cut off by a network fault is now sent again, as a rate-limited one already was. A wait between retries now ends the moment the caller's deadline passes, for the rerankers too.
A memory whose stored JSON is damaged is now named in the log. A tags_json, conflicts_with_json or parents_json column, or a session event's metadata_json, that is not valid JSON still reads as empty, and hippo now logs one warn line with the table, the row id and the column. Each damaged column of each row is reported once per process (once per store worker thread under hippo serve); later reads of it log at debug. After 1,000 once-only warnings in one process, one warn line says so and new ones log at debug. The line never holds the stored text.
hippo serve now logs how a request ended after its 504 deadline reply. A handler that fails after the reply logs one warn line, request failed after its deadline reply. A request that is not a GET and runs to its end after the reply logs one info line, request finished after its deadline reply; its own reply was dropped, so a write the caller was told may not have been saved can be confirmed. Both lines carry the request id, the method and the route name of the access line, and no path, body or error text.
A session-end step that fails is now written to the session log. When the background worker's sleep, its capture, or its transcript scan and digest fails, hippo logs one warn line and appends the same line to the session log file (session-end: sleep failed: ..., codex session-end: transcript scan or digest failed: ...). Before, a failure outside the sleep and capture commands' own logging was written at debug only, and the worker's output goes nowhere, so nothing was left on file.
hippo codex-run no longer crashes when its background session-end worker cannot start. A start failure the system reports late (no memory, no process slots, a Node binary replaced mid-session) was an unhandled error, and the session's sleep and capture were lost. The work now runs in the wrapper itself before it exits, as it already did for a failure reported at once.
A Slack or GitHub backfill now stops with a clear error when the service sends a reply that is not what its API documents. An HTML error page served with status 200, a null body, a GitHub body that is not a list, or a body over the size limit (64 MiB for a Slack page, 32 MiB for a GitHub page) used to fail with a bare parse error, or pass through as an empty page. The error now names the service, the HTTP status and the URL path, never the token or the query string. The Jev judge and the Anthropic client read their replies under a 1 MiB limit with the same shape checks; an unreadable Anthropic reply is now reported as reply is not JSON in place of the parser's own text.
A config file hippo rewrites (settings.json, opencode.json, the Codex wrapper, the workspace registry, an instructions file) now survives a power loss during the write. The temp file is flushed to disk before it is renamed over the old one, and on Linux and macOS the folder is flushed after. When the system refuses the rename and hippo has to write the file in place, it now logs one warning that names the file and says the write was not atomic.
A delivery ledger failure is now logged like every other hippo diagnostic. The four [hippo] delivery ledger ... lines went straight to stderr. They now carry a level ([hippo] error: delivery ledger write failed: ..., [hippo] warn: delivery ledger skipped: ...), a timestamp and, inside a server request, the request id, and they follow HIPPO_LOG and HIPPO_LOG_FORMAT=json. One of them was also missing the space after its colon.
hippo dashboard now says what to do when its port is taken, and stops cleanly on Ctrl+C. A busy port printed an uncaught-error log line; it now prints one plain line that names the port and points at --port, and exits 1. Ctrl+C or a stop signal closes the listener and the store connection, then exits 0, within the 15 seconds hippo serve allows. The dashboard also runs under the same socket timeouts as the API server (65 s keep-alive, 70 s for headers), so an idle browser connection is no longer closed after 5 seconds, just as the browser reuses it.
The hippo_outcome tool description, in the MCP server and the OpenClaw plugin, no longer states half-life changes (+5 days, -3 days) that the model does not make.
hippo github takes its tenant from the dispatcher. Backfill and the DLQ verbs now use the same tenant the other verbs use (HIPPO_TENANT, else default); the value is the same one they resolved before, so nothing changes for users.
A hook that cannot start its delivery recorder now logs through the leveled logger. The skip line was the one delivery-ledger message left outside it, so HIPPO_LOG and HIPPO_LOG_FORMAT=json did not apply. It now reads [hippo] warn: delivery ledger skipped: <reason>, with a space after the colon.
The local embedder now loads its optional package with a plain dynamic import. It also loads inside test runners that execute modules in a VM, where the old Function-built import failed with ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING.
Security
The daily-runner installer now refuses a store path that holds %, which crontab reads as a newline and would split the installed line.
The LLM and Jev rerankers now read the model reply through the same 1 MB cap as the CLEF reranker, so a hostile endpoint cannot make recall buffer an unbounded body.
Embedding provider replies are now read under a size cap that scales with the batch, and an error body is read for its first 1,200 bytes only.
hippo serve now refuses a request with no API key, from this machine too. A request from the same machine used to be let in with no key and act as host admin, so any local process, and any other container in the same pod, could read, write and administer the store. Now every request needs a key unless the server is started with HIPPO_ALLOW_KEYLESS_LOCAL=1. HIPPO_REQUIRE_AUTH=1 works as before and wins when both are set. The refusal is a 401 whose text names both fixes, and hippo serve prints the rule it is running under at every start.
GET /health gives its version and process id only to a caller on this machine. A page in a browser that reaches the server through another site, or through a hostile name that resolves to 127.0.0.1, now reads {"ok":true} and nothing else, as a caller from another machine already did. This check does not depend on the key rule.
Upgrading. Nothing changes if you never run hippo serve, if your callers already send a key, or if the server runs with HIPPO_REQUIRE_AUTH=1 (the Docker image in deploy/aml does). If you run hippo serve and call it with no key, those calls now get 401 auth required: this server takes no request without an API key.... That covers curl, the Python client built with no api_key, an MCP client aimed at /mcp with no Authorization header, and the CLI itself: while a server runs for a store, hippo remember, hippo forget and hippo promote in that store go through it, so they print Error: auth required: ... and exit non-zero, and so does any hook or script that calls them. The one step that restores the old behaviour is to restart the server with HIPPO_ALLOW_KEYLESS_LOCAL=1. The better step is a key: run hippo auth create on the host (it needs no key and no server), then set HIPPO_API_KEY for the CLI, pass api_key= to the Python client, or send Authorization: Bearer <key>.
A new API key is a member key that expires after 90 days unless you ask for something else.hippo auth create and POST /v1/auth/keys used to mint an admin key that never expired whenever the role was left out. Now a mint with no role makes a member key, and a mint with no expiry makes a key that stops working 90 days later. Ask for the wider key by name: --role admin for an admin key, --ttl-days <n> for another lifetime (at most 3650 days), --no-expiry for a key that never expires. Over HTTP the same choices are "role": "admin", "ttlDays": <n> and "noExpiry": true; in the Python client they are role="admin", ttl_days=<n> and no_expiry=True.
The mint tells you what it made. The CLI prints role: and expires: lines ("role" and "expiresAt" with --json), and names on stderr each default it took. The HTTP reply carries role and expiresAt (null for a key that never expires), and the auth_create audit row records both.
Upgrading. Keys you already have are not changed: their role stays, and a key with no expiry still has none. What changes is the next mint. A script that runs hippo auth create or posts {} to /v1/auth/keys and then calls an admin route (/v1/sleep, /v1/quarantine, minting a key, revoking another key) gets a 403 until it asks for --role admin or "role": "admin". A key minted with the default lifetime answers 401 invalid api key from day 90; mint its replacement before then, or pass --no-expiry. The first key of a store gets no special treatment: the CLI on the host needs no key, so hippo auth create --role admin always works there.
The first key with an expiry stops hippo older than 1.64.0 from opening that store. Those releases ignore the expiry and would keep honouring an expired key, so the store refuses them from the first default mint on. If another machine or tool shares the store with an older hippo, upgrade it first or mint with --no-expiry. docs/ARCHITECTURE.md (section src/db/migrations/v53.ts) has the way back.