feat(integrations): windowed injection-cooldown parity everywhere - #63
Closed
perfectra1n wants to merge 6 commits into
Closed
perfectra1n wants to merge 6 commits into
perfectra1n wants to merge 6 commits into
Conversation
Two cuts to SessionStart's fixed overhead: The directive shrinks from ~2,070 to ~640 chars. It was a full copy of the save policy whose canonical text already ships in the MCP server instructions every session — the hook copy only needs to re-arm the habit, so it is now explicitly the abridged trigger and the mcp.go serverInstructions text is the single source of truth. Emission now depends on payload.source instead of firing on every path: startup/clear (and hosts without a source) get the directive; resume gets nothing — Claude Code replays previously injected hook text on resume, so re-emitting was pure duplication; compact gets only the compact-recovery nudge — the context was rebuilt, but the MCP server instructions persist in the system prompt, so re-sending the save policy duplicated them too. Net per-session cost of the directive drops ~72% on startup and to zero on resume/compact.
…ow it
Closes the loop the activity feed was missing: recall serves were logged
server-side BEFORE the client's dedupe/cooldown gates ran, so the feed
over-reported what reached model context and made recall look like it
was spamming the same memories.
Server: POST /v1/activity/injected accepts a per-surface beacon
(injected ids + token/char estimates + suppressed counts by reason),
recorded as a new 'inject' activity event through the best-effort event
funnel, counted into memini_injected_memories_total{surface,result} and
memini_injected_tokens_total{surface}. The activity regrouping joins
inject reports onto nearby recall serves (same namespace, 5-minute
window — recall events carry no session id) and annotates each served
memory with injected: true/false; ABSENT means no report covered the
serve, so old data and non-reporting integrations render unchanged.
Client (Claude Code/Codex hooks): after composing stdout, each
injecting surface (briefing / prompt / pretool) sends one bounded
beacon — AbortSignal 500ms, awaited before exit, every error swallowed,
never touches stdout — gated by the new inject_telemetry setting
(MEMINI_INJECT_TELEMETRY, default on, served via the handshake like its
siblings).
UI: recall rows now read "served N → injected M" with a 'not
injected' chip on reported-suppressed memories; inject events render
their surface, per-reason suppression counts, and the token estimate.
Grafana gains an Injection efficiency row (outcomes by result, injected
by surface, injected tokens/hour).
…emory_get
Injection payloads switch from truncated-content blobs to an index the
model can expand on demand.
Server: a shared internal/api/render package projects concise content
(summary if present, else a word/sentence-boundary cut — no more
mid-sentence artifacts) behind response_format=concise on /v1/search
and format=concise on the briefing; the briefing gains
children=summary|full|none, with summary matching the isolation
contract docs always claimed (titles/counts only) while the admin UI
keeps full. Every memory now carries content_hash (16-hex sha256 of
full content||summary) in every format, and GET /v1/memories/{id} +
MCP memory_get resolve unambiguous id prefixes >= 8 hex chars (409
lists the collisions; mutations stay verbatim-id-only).
Client: hooks request concise (400-fallback for older servers), render
hits as one-liners with a trailing [m:<id8>] handle, and teach the
fetch path with a single comment line — only on blocks that actually
truncated. The briefing Recent section drops to strict index mode
(120 code points + age + handle). Dedupe identity now prefers the
server content_hash, so a memory served full in the briefing and
concise in a recall dedupes as the same memory — the regression that
made concise unsafe before.
Tail drops now say what they dropped: [+N more — memory_recall for
detail].
The inject_*_max_tok knobs stop being silent client-side trims and become request budgets the server honors: /v1/search gains max_tokens (fill in final rank order, first result always fits, response reports omitted) and the briefing gains ?max_tokens (fill pinned -> facts -> procedures -> recent, whole items only, response reports omitted). Estimation matches the client's approxTokens recipe over the content the response actually ships (concise text in concise mode) plus a 10-token per-item skeleton overhead. What the budget drops is visible: recall activity events record budget_omitted, and the hooks render the server's count in the [+N more — memory_recall for detail] footer. Client-side fitByTokens stays wired as the old-server fallback; a 400 on max_tokens strips it first in the existing newest-field-first retry chain. Defaults come off unbounded: briefing 600 tokens, recall 250, pretool 200 per file — sized so the median injection is untouched and the multi-KB tail is what gets clipped.
… vectors The injection-limiting primitives — suppression windows, seen-set merge, token budgeting, render formats, identity/fingerprints, the beacon payload — lived as hand-written .mjs in the Claude plugin and were copy-pasted (and drifting) per integration. They now live once, as pure functions in packages/memini-client/src/enforce/ (no fs, no env, no clock — state and now are parameters), bundled into the plugin via the existing _client.gen.mjs path; _shared.mjs keeps only the Node adapters (file I/O, transport) and re-exports the same names, so hooks and their 202 tests are untouched. packages/memini-client/vectors/enforcement.json pins 54 golden cases generated once from the original implementations — the cross-language contract any future port (hermes is Python) is tested against. pi switches its local copies to the core. Two real drifts surfaced: its injectedIdentity had fallen behind (no content_hash preference) and its fitByTokens lacked the partial-fit trim — both adopted; pi-specific render wordings its tests pin stay local.
The shared enforce core's semantics now hold across every integration, not just the Claude/Codex plugin and pi: openwebui had NO dedupe — it re-served identical memories every turn. It gains the standard per-chat windowed cooldown (30min/3-prompt defaults from the handshake settings), exclude_ids wire exclusion with the old-server retry latch, and the content-identity bypass. opencode suppressed forever within a session — the attention-decay failure the windowed design exists to avoid. Both shipped entry points move to windowed entries honoring inject_cooldown_ms/prompts; re-reminding after the windows lapse is a deliberate, README-flagged behavior change (0/0 restores forever). openclaw's prompt-count window was inert. It now counts what that host can honestly count — completed agent turns (agent_end) — so steps never burn the window; a host without the signal degrades to time-only. hermes tracked injected ids without content identity, so an updated memory stayed withheld for the full window. It adopts the content_hash identity + mismatch bypass, and gains a golden-vector runner replaying packages/memini-client/vectors/enforcement.json — the cross-language drift gate for its Python port.
Owner
/sync |
Owner
|
Promoted to the canonical Forgejo repository (PR #153), which is the source of truth. Further review and merging happen there. |
eleboucher
pushed a commit
that referenced
this pull request
Jul 19, 2026
> _Originally by **perfectra1n** on 2026-07-19 09:05 UTC — [view on source](#57 Adds a `memini_http_response_bytes` histogram (chi route pattern + method, buckets 256B–256K) observed from the bytes the wrapped ResponseWriter already counts, plus a p50/p90 dashboard panel for the recall and briefing routes. **Why:** the hook-driven injection pipeline ships several MB of `/v1/search` payload per day (measured on a production deployment: mean 6.8KB, p90 11.8KB per search response, ~1,300 searches/day), but only request counts and latency were measured. Response volume — the thing injection tuning changes — was invisible. This is the baseline metric for the injection-efficiency series. Standalone; no behavior change. The series: #58 (directive slimming) → #59 (injection telemetry) → #60 (progressive disclosure) → #61 (server budgets) → #62 (enforcement core) → #63 (integration parity), plus #64 (floors, stacked on #56) and #65 (capture hygiene) — each independently mergeable in order. <!-- forgesync:src=github;host=github.com;repo=eleboucher/memini;kind=pull_request;id=57 --> Co-authored-by: perfectra1n <[email protected]> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/memini/pulls/156
eleboucher
added a commit
that referenced
this pull request
Jul 19, 2026
…tion cooldown parity (#163) > _Originally by **perfectra1n** on 2026-07-19 16:29 UTC — [view on source](#67 This combines the four PRs from the injection series that were still open after the token-budget stack landed (your 151/154/156): #56 (server rank floor), #62 (shared enforcement core), #63 (cooldown parity for the integrations), and #64 (0.5 floors, no more Glob/Grep recall). They were developed as two parallel stacks that both rewrite the client enforcement code, so rebasing each one separately would have handed you four rounds of overlapping conflicts. This branch reconciles them once, on top of current main, with everything re-tested against the merged result. ## Why these still matter The merged budget stack caps how much an injection can ship. This half of the series decides what deserves to ship at all: * The floor moves to the server. `min_rank_score` floors the final composite score (the one the activity feed shows), so one knob value means the same thing in every integration, and floored hits stay visible in the feed (dimmed, marked as filtered) instead of silently vanishing client-side. * The floor default goes to 0.5 and pretool stops recalling on Glob/Grep. On the live deployment, pattern-derived queries ("Grep on `<pattern>`") were the bulk of near-zero-signal serves, and each one costs a server embed plus rerank. * The enforcement primitives (identity hashing, the cooldown predicate and its state algebra, token budgeting, rendering, telemetry) move into `packages/memini-client/src/enforce/`, pinned by golden vectors, so pi/openclaw import them directly and the hooks consume the committed bundle. One implementation instead of five drifting copies. * The integrations get the same windowed cooldown the Claude Code hooks already have: a memory suppressed while in its time or prompt window, re-admitted when both lapse, content changes always resurfacing. ## Where combining them was real work (worth a closer look in review) * The pretool exclude_ids latch (#56) was written against the old `_shared.mjs` function bodies, which #62 moved into `enforce/seen.ts`. The latch is implemented in the new home: `recordInjected` stamps `r: 0` and resets an earned latch, `pretoolExcludeIds` reuses the exact `cooldownIds` window predicate, and `normInjectedEntry` preserves `r` without backfilling it on read. That last bit is deliberate: the golden vectors were cut from the pre-latch implementation, and not backfilling keeps every vector byte-identical, so `vectors/enforcement.json` is untouched by this branch. * Recall pipeline order is now finalize, floor, linked expansion, budget. The floor runs on the final score before expansion (linked hits carry a synthetic score a post-expansion floor would nullify); the budget trims last so it sees exactly the list the caller would receive. The activity event carries `floored`, `budget_omitted`, and `excluded_count` side by side, and the feed's recall headline composes both features ("served 2 -> injected 1", "served 0 / 3 floored"). * The old-server 400 fallback keeps the strip-one-field-per-retry chain from the budget stack, with `min_rank_score` joining at the front: min_rank_score, then max_tokens, then exclude_ids, then response_format. Since the 0.5 floor now rides every search, a server that predates it costs one extra retry on the first shape; every intermediate shape is valid, and a stripped floor falls back to the client-side filter. * The opencode and openclaw floor ports predate the parity refactor, so they were re-seated onto the parity versions: v1 keeps its cooldown-derived exclude_ids, v2 and openclaw keep the windowed seen state, and openclaw wires the floor knob alongside both cooldown dimensions. * Defaults merged rather than fought over: the landed token budgets (600 briefing / 250 recall / 200 pretool) stay, the min-score floors go to 0.5, Glob/Grep leave the default pretool allowlist (listing them restores the old behavior). ## Testing * `go test ./...` green. * Client core 180/0, enforcement vectors untouched and passing. * Claude hooks 215/0, opencode 118/0, openclaw 151/0, hermes 87 OK, openwebui 48 OK. * pi: 94 pass, 1 fail. The failing test ("precompact and shutdown checkpoints are bounded, gated, ...") fails identically on current main, so it is pre-existing and not from this branch. Happy to chase it separately. * All generated artifacts (api.gen.go, `_client.gen.mjs`, UI catalog, docs reference) regenerated, zero drift. Supersedes #56, #62, #63, and #64. The commits keep the original series structure (core, parity, floor stack, defaults) in case you want to review or land them piecewise anyway. <!-- forgesync:src=github;host=github.com;repo=eleboucher/memini;kind=pull_request;id=67 --> Co-authored-by: perfectra1n <[email protected]> Co-authored-by: Erwan Leboucher <[email protected]> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/memini/pulls/163
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #62 (last of the stack).
The enforce core's semantics now hold across every integration:
exclude_idswire exclusion with the retry-once-and-latch old-server fallback, and the content-identity bypass.agent_end) — the once-per-turn signal that host can honestly maintain; hosts without it degrade to time-only, never forever.vectors/enforcement.json— the Python drift gate.Suite counts: opencode 84→115, openwebui 37→48, hermes 74→84, openclaw 75+70→77+72; plugin/client/pi untouched.